# CRM invoice and payment workflow

Lead details support invoices for existing course reservations and invoices
created before a reservation is selected. For a track without a reservation,
staff select Native or Native-like explicitly. With a reservation, pricing and
teacher type come from that reservation. Preview and creation use the same
pricing resolver and preserve the selected package and teacher type.

Scheduled invoices allow the same deposit, promo, and referral options as
unscheduled invoices. Opening an unscheduled invoice does not enroll the student
into a course. A request reference prevents retries from creating another
Finance invoice, while separate purchases can have their own invoices.

## Gateways and activation

The CRM supports PayPal, Kashier, Stripe, and manual InstaPay transfers. Gateway
options are supplied by active Finance treasuries with `payment_flow=online`
and the corresponding gateway name. Manual transfer options come from active
bank treasuries configured for manual payments. Use the normal Finance currency
and exchange-rate configuration: the charged amount/currency is the treasury
quote captured on the transaction, not a hardcoded currency.

Stripe environment settings:

```
STRIPE_SECRET=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_TOLERANCE=300
STRIPE_API_BASE_URL=https://api.stripe.com
STRIPE_LINK_TTL_MINUTES=60
STRIPE_TIMEOUT=30
```

Register `POST /webhooks/stripe` on the CRM domain for
`checkout.session.completed`, `checkout.session.async_payment_succeeded`,
`checkout.session.async_payment_failed`, and `checkout.session.expired`.
The return routes are `/payment/stripe/success` and `/payment/stripe/cancel`.
Only a paid Checkout Session is recorded as paid, after the amount, currency,
session, and invoice/transaction metadata have been checked. Browser returns
retrieve the session from Stripe; a browser-supplied status is never sufficient.
Set `STUDENT_SUCCESS_PAYMENT_URL` and `STUDENT_CANCEL_PAYMENT_URL` to the student
application return pages. Configure `FINANCE_BASE_URL` for the Finance service sharing this database.
`FINANCE_API_KEY` is optional under the current Finance integration routes,
which do not attach the API-key middleware. If authentication is enabled later,
configure the matching Finance API key. Gateway credentials are configured in the local `.env`; keep credentials out of
version control. Stripe webhook secrets must match the registered CRM endpoint.
Finance API keys and Finance webhook secrets are separate settings.

Kashier uses the existing `KASHIER_MERCHANT_ID` and `KASHIER_API_KEY` settings.
For Payment Request API mode, also configure `KASHIER_DASHBOARD_API_KEY` and
`KASHIER_USE_PAYMENT_REQUEST_API`. Keep test/live keys and API hosts consistent.
The return routes remain `/payment/kashier/success` and
`/payment/kashier/cancel`; the webhook is `/webhooks/kashier`.

Kashier redirect signatures cover the fixed ordered response fields. Standard
webhooks verify sorted `data.signatureKeys` with the Payment API Key. Existing
raw-body webhook integrations remain available only when an explicit
`KASHIER_WEBHOOK_SECRET` is configured. No missing secret bypasses verification.
Only pay/capture notifications with matching charged amounts and currencies
can confirm payment. Failed, authorization-only, and refund events do not mark
an invoice paid; late failure notifications do not downgrade a confirmed payment.

Confirmed payments use the existing Finance integration and transaction-specific
idempotency keys. A transaction already synced to Finance is not posted again.
Stripe and Kashier use distinct payment references. InstaPay remains pending
until staff approve it; pending online transactions cannot be manually marked
paid without gateway confirmation. Remaining balances are calculated from
confirmed transaction amounts.

These are code changes only: no database migration, production payment,
webhook registration, credential update, or external message is performed.

## Tests

```
php vendor/phpunit/phpunit/phpunit tests/Feature/CrmGatewayPaymentsTest.php tests/Feature/CrmInvoiceWorkflowTest.php
```

Tests use isolated SQLite, fake HTTP responses, and minimal framework providers.
They do not connect to the shared application database or real gateways.

Provider references:

- https://developers.kashier.io/docs/webhooks
- https://developers.kashier.io/docs/direct-api/hashing
- https://docs.stripe.com/webhooks
