# 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 direct charges:

- 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 connected organiser account by passing the connected account in the Stripe request options.
- The PaymentIntent includes `application_fee_amount` for Zavvion Events' platform service fee plus service-fee tax.
- The organiser connected account receives the charge net of Stripe processing fees and the Zavvion application fee.
- The Zavvion platform receives the application fee automatically through Stripe Connect.
- 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 collect the event money and Zavvion collects customer-paid platform fees, platform marketing charges, and VAT/tax on platform fees where applicable. The code keeps destination-charge support available only as an explicit reviewed configuration if merchant-of-record, liability, reporting visibility, or Stripe fee-payer decisions change.

The configured Connect model is explicit: `STRIPE_CONNECT_CHARGE_MODEL=direct_charge`. For new connected-account onboarding, use Stripe Accounts v2 with explicit controller/responsibility settings. Do not switch to `destination_charge` without a reviewed merchant-of-record, liability, Stripe fee-payer, and reporting decision.

Implementation contract for the MVP checkout route:

- Connected-account checkout uses direct charges by default: `application_fee_amount` is included in `payment_intent_data`, and the Stripe request options include the connected organiser account ID.
- Destination charges remain a supported factory mode only for a reviewed migration: the request includes `payment_intent_data.transfer_data.destination` and does not pass the connected account option.
- 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, direct-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 checkout. Run `php bin/expire-seat-holds` every minute in production/staging so abandoned baskets 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.
