# Payments

Payments are abstracted behind `PaymentGatewayInterface` so the checkout workflow can support future gateways without rewriting order, ticket, ledger, or scanner logic.

## Chosen Stripe Connect Model

For Zavvion Events, the launch model is Stripe Checkout Sessions backed by PaymentIntents with Connect destination charges using explicit transfer amounts:

- Public online paid checkout is Stripe-hosted card payment only; cash and external-card recording stay in organiser box office workflows.
- The Checkout Session is created on the platform account.
- The PaymentIntent includes `transfer_data[destination]` for the organiser connected account and `transfer_data[amount]` for organiser gross only.
- The Zavvion platform keeps the customer-paid service fee on the platform charge.
- The organiser connected account receives only the organiser gross transfer, not the full customer total.
- The Stripe webhook is the only trusted source that marks an order paid and issues tickets.
- Expired or failed Stripe checkout sessions release any still-active reserved-seat holds for that checkout.

This follows the product policy that organisers receive event money and Zavvion keeps customer-paid platform fees, platform marketing charges, and VAT/tax on platform fees where applicable.

The configured Connect model is explicit: `STRIPE_CONNECT_CHARGE_MODEL=destination_charge`. For new connected-account onboarding, use Stripe Accounts v2 with explicit controller/responsibility settings. Do not switch to `direct_charge` without a reviewed connected-account accounting and dashboard-visibility decision.

Implementation contract for the MVP checkout route:

- Connected-account checkout uses destination charges by default: `payment_intent_data.transfer_data.destination` is the organiser connected account and `payment_intent_data.transfer_data.amount` is customer total minus platform service fee.
- Direct charges remain a supported factory mode only for a reviewed migration: `application_fee_amount` is included in `payment_intent_data`, and the Stripe request options include the connected organiser account ID.
- If no transfer-ready connected account is selected, the Checkout Session is treated as a platform-charge fallback and must not be considered a completed Connect fee/transfer validation.

## Local Mode

Use test keys in `.env`. Never commit real Stripe secrets. If credentials are missing, the checkout draft is kept pending and the UI shows that payment is unavailable.

Mock payment is now fail-closed. It is only available when all of these are true:

- `APP_ENV` is `local`, `dev`, `development`, `test`, or `testing`.
- `PAYMENT_MOCK_ENABLED=true`.
- The checkout endpoint records `payment_mode=local_mock` for that draft.

Production and staging environments must not use the mock payment route. Configure Stripe test credentials first, then confirm payment by Stripe Checkout webhook.

Zero-total complimentary checkouts use `POST /api/v1/checkout/confirm-free`. That route is guarded by `FreeCheckoutPolicy`, only accepts saved checkout drafts whose server-calculated total is `0`, and records the completion provider as `free_checkout`.

The admin System Health screen includes a Stripe readiness panel. It checks for the Stripe PHP SDK, test secret key, publishable key, webhook signing secret, transfer-ready connected account availability, destination-charge mode, and test mode. This gives you a clear place to verify the system before you connect your Stripe account for testing.

Reserved seats are held for 15 minutes during public checkout. Box Office Stripe payment links use the organiser-configured expiry window, defaulting to 60 minutes. Run `php bin/expire-seat-holds` every minute in production/staging so abandoned baskets and unpaid Box Office payment links are released even during low traffic.

## Accounting Invariant

Every successful paid checkout must write a balanced double-entry ledger. The launch invariant is:

```text
customer gross debit = platform fee gross credit + organiser gross credit
```

`platform_fee_gross` is split into `platform_fee_net` and `platform_fee_tax` in order and reporting data. The admin finance report must treat mismatches as review exceptions, not as acceptable unreconciled differences. See `docs/accounting-double-entry.md` for the journal model and report definitions.

## Refund Responsibility

Current launch policy keeps Stripe-originated disputes and platform-managed refund automation out of scope. Refund tooling remains feature-gated by platform admin policy.

When an organiser collects payment through a connected payment account, the organiser is responsible for refund decisions and for processing any approved refunds under the organiser's published event policy. Zavvion Events stores the order, ticket, ledger, audit, and reporting records needed to support that workflow, but the organiser remains responsible for customer handling and approved refund execution unless a separate written platform policy overrides it.
