Files
fotospiel-app/docs/prp/08-billing.md
Codex Agent a949c8d3af - Wired the checkout wizard for Google “comfort login”: added Socialite controller + dependency, new Google env
hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads,
attach packages, and surface localized success/error states.
- Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/
PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent
creation, webhooks, and the wizard CTA.
- Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/
useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages,
Checkout) with localized copy and experiment tracking.
- Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing
localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations.
- Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke
test for the hero CTA while reconciling outstanding checklist items.
2025-10-19 11:41:03 +02:00

6.4 KiB

Billing and Payments

Overview

The Fotospiel platform supports multiple payment providers for package purchases: Stripe for one-time and subscription payments, and PayPal for orders and subscriptions. Billing is handled through a freemium model with endcustomer event packages and reseller subscriptions. All payments are processed via API integrations, with webhooks for asynchronous updates.

Stripe Integration

  • One-time Payments: Use Stripe Checkout for endcustomer event packages. Create PaymentIntent via StripeController@createPaymentIntent.
  • Subscriptions: Reseller subscriptions use Stripe Subscriptions API. Webhook handling in StripeWebhookController@handleWebhook for events like invoice.paid, customer.subscription.deleted.
  • Configuration: Keys in config/services.php under stripe. Sandbox mode based on APP_ENV.
  • Models: PackagePurchase records all transactions with provider_id (Stripe PI ID), status, metadata.
  • Frontend: PurchaseWizard.tsx handles client-side Stripe Elements for card input and confirmation.
  • Webhook Matrix:
    Event Purpose Internal handler
    payment_intent.succeeded Completes single-event package purchase StripeWebhookController@handlePaymentIntentSucceeded
    payment_intent.payment_failed Logs failure, triggers recovery emails handlePaymentIntentFailed
    invoice.paid Confirms subscription renewal, extends package handleInvoicePaid
    invoice.payment_failed Flags tenant for follow-up, sends alerts handleInvoicePaymentFailed
    customer.subscription.deleted Finalises cancellation/downgrade handleSubscriptionDeleted

PayPal Integration

  • SDK: Migrated to PayPal Server SDK v1.0+ (paypal/paypal-server-sdk). Uses Builder pattern for requests and Controllers for API calls.
  • Orders (One-time Payments): Endcustomer event packages via Orders API. PayPalController@createOrder uses OrderRequestBuilder with CheckoutPaymentIntent::CAPTURE, custom_id for metadata (tenant_id, package_id, type). Capture in @captureOrder using OrdersController->captureOrder. DB creation in processPurchaseFromOrder.
  • Subscriptions (Recurring Payments): Reseller subscriptions via Orders API with StoredPaymentSource for recurring setup (no dedicated SubscriptionsController in SDK). PayPalController@createSubscription uses OrderRequestBuilder with StoredPaymentSource (payment_initiator: CUSTOMER, payment_type: RECURRING, usage: FIRST), custom_id including plan_id. Initial order capture sets up subscription; subsequent billing via PayPal dashboard or webhooks. DB records created on initial capture, with expires_at for annual billing.
  • Differences: One-time: Standard Order with payment_type ONE_TIME (default). Recurring: Order with StoredPaymentSource RECURRING to enable future charges without new approvals. plan_id stored in metadata for reference; no separate subscription ID from SDK.
  • Client Setup: OAuth2 Client Credentials flow. Builder: PaypalServerSdkClientBuilder::init()->clientCredentialsAuthCredentials(ClientCredentialsAuthCredentialsBuilder::init(client_id, secret))->environment(Environment::SANDBOX/PRODUCTION)->build().
  • Webhooks: PayPalWebhookController@verify handles events like PAYMENT.CAPTURE.COMPLETED (process initial/renewal purchase), BILLING.SUBSCRIPTION.CANCELLED or equivalent order events (deactivate package). Simplified signature verification (TODO: Implement VerifyWebhookSignature).
  • Webhook Matrix:
    Event Purpose Internal handler
    PAYMENT.CAPTURE.COMPLETED Confirms one-time order, activates tenant package handleCapture
    BILLING.SUBSCRIPTION.ACTIVATED, BILLING.SUBSCRIPTION.UPDATED Syncs reseller subscription status/expiry handleSubscription
    BILLING.SUBSCRIPTION.CANCELLED, BILLING.SUBSCRIPTION.EXPIRED Marks package inactive and downgrades tenant handleSubscription
    BILLING.SUBSCRIPTION.SUSPENDED Pauses package benefits pending review handleSubscription
  • Idempotency: Check provider_id in PackagePurchase before creation. Transactions for DB safety.
  • Configuration: Keys in config/services.php under paypal. Sandbox mode via paypal.sandbox.
  • Migration Notes: Replaced old Checkout SDK (PayPalCheckoutSdk). Updated imports, requests (e.g., OrdersCreateRequest -> OrderRequestBuilder, Subscriptions -> StoredPaymentSource in Orders). Responses: Use getStatusCode() and getResult(). Tests mocked new structures. No breaking changes in auth or metadata handling; recurring flows now unified under Orders API.

Database Models

  • PackagePurchase: Records purchases with tenant_id, package_id, provider_id (Stripe PI/PayPal Order ID), price, type (endcustomer_event/reseller_subscription), status (completed/refunded), metadata (JSON with provider details).
  • TenantPackage: Active packages with tenant_id, package_id, price, purchased_at, expires_at, active flag. Updated on purchase/cancellation.
  • Constraints: type CHECK (endcustomer_event, reseller_subscription), price NOT NULL.

Flows

  1. Purchase Initiation: User selects package in PurchaseWizard. For free: direct assignment. Paid: Redirect to provider (Stripe Checkout or PayPal approve link).
  2. Completion: Provider callback/webhook triggers capture/confirmation. Create PackagePurchase and TenantPackage. Update tenant subscription_status to 'active'.
  3. Cancellation/Refund: Webhook updates status to 'cancelled', deactivates TenantPackage.
  4. Trial: First reseller subscription gets 14-day trial (expires_at = now() + 14 days).

Error Handling

  • Validation: Request validation for IDs, consent.
  • API Errors: Catch exceptions, log, return 400/500 JSON.
  • Idempotency: Prevent duplicate processing.
  • Webhook: Verify signature, handle unhandled events with logging.

Testing

  • Unit: Mock providers in PurchaseTest.php for order creation, capture, webhooks.
  • Integration: Sandbox keys for end-to-end. Assertions on DB state, responses.
  • Edge Cases: Failures, idempotency, trials, limits.

Security & Compliance

  • GDPR: No PII in logs/metadata beyond necessary (tenant_id anonymous).
  • Auth: Sanctum tokens for API, CSRF for web.
  • Webhooks: IP whitelisting (PayPal IPs), signature verification.
  • Retention: Purchases retained per Privacy policy; update on changes.