Files
fotospiel-app/docs/prp/08-billing.md

68 lines
6.4 KiB
Markdown

# Billing and Payments
## Overview
The Fotospiel platform supports multiple payment providers for package purchases: Stripe for one-time and subscription payments, and Paddle 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` |
## Paddle Integration
- **SDK**: Migrated to Paddle 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. `PaddleController@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). `PaddleController@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 Paddle 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**: `PaddleWebhookController@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 (`PaddleCheckoutSdk`). 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/Paddle 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 Paddle 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 (Paddle IPs), signature verification.
- Retention: Purchases retained per Privacy policy; update on changes.