# Stripe, purchase withdrawal and refunds

## Current boundary

The code uses the official `stripe/stripe-php` SDK, Stripe-hosted Checkout in `mode=payment`, and a small purpose-specific gateway/service. No Cashier/subscription framework or Connect marketplace is involved. `STRIPE_MODE=test` is the default. `STRIPE_MODE=live` additionally requires `APP_ENV=production` and `STRIPE_LIVE_APPROVED=true`; keys, Prices, signed webhooks and canonical payments must match the selected mode. New production checkout also requires the public launch approval. Refund/reconciliation access remains available if new checkout is disabled. No live products, payments, account configuration or charges were created. Actual sandbox checkout/refund testing requires protected credentials and an approved test Price. Mocked API responses are never reported as real Stripe transactions.

The local CLI was installed under `/private/tmp`, version 1.53.1. Directory lookup was attempted first but returned “Invalid user agent”; the skill's `/references/payments.md` and `/references/security.md` paths returned 404. Official CLI documentation for hosted Checkout fulfillment, webhooks, refunds, keys and payment-method configurations was then read. API requests explicitly use `2026-08-26.dahlia`; actual SDK version is recorded in the lockfile.

## Configure a sandbox

Use a dedicated Stripe sandbox for local/CI work. Provide a test restricted key with just the required Price/Checkout/PaymentIntent/Charge/refund read/write permissions, the endpoint secret and a `price_…` ID. The Price must be active, one-time, inclusive tax behavior, SEK and exactly 49900 minor units. Settings are `STRIPE_SECRET`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID`, `BILLING_ENABLED`, `LEGAL_APPROVED`, `TAX_APPROVED`, operator fields and contact. Keep all keys out of frontend variables, logs and reports.

Configure cards/wallets through `STRIPE_PAYMENT_METHOD_CONFIGURATION` in each Stripe environment. Live checkout requires a `pmc_…` configuration ID; confirm the actual Dashboard configuration enables only the approved launch methods. Checkout passes `payment_method_configuration` and does not hardcode `payment_method_types`. Both completed and asynchronous-success webhooks still require canonical paid state before fulfillment.

Use separate sandbox/staging and production databases, keys, endpoint secrets, Price IDs and payment-method configuration IDs. Do not promote a sandbox database containing test purchases into live selling. Prefer a restricted key with the minimum API permissions, kept in the host's secret store or a protected environment file outside the document root. Live approval records the owner's completed external sandbox verification; tests do not set it in the delivered environment.

Do not enable `STRIPE_AUTOMATIC_TAX` until the owner/accountant confirms active registrations, classification, inclusive tax setup and supported selling territories. Never treat the switch as a registration/filing service. Review the intended SE launch market and additional mandatory consumer rights before adding markets. Billing addresses stay in hosted Stripe services, outside the relocation profile. Canonical tax amount is retained only as a minor-unit financial value.

## Contract and access model

Server-side user identity, approved Price, quantity one, amount/currency and immutable offer/Terms/policy/activation snapshots define a purchase. A unique per-user purchase lock and user-row transaction prevent duplicate pending passes. Failed network requests retry the same opaque purchase/idempotency key. No route, municipality, SFI status or checklist content is sent to Stripe metadata.

Contract formation is recorded from the successful canonical card charge after a paid Checkout is verified. This business interpretation and selling-market scope need legal review. Payment settlement, contract formation, planned activation, actual activation, access expiry, notice receipt and refund progress are separate fields. Immediate access is an unchecked optional request, not a waiver. Deferred access starts after the applicable initial withdrawal period, with all 12 calendar months measured from actual activation. Leap-day arithmetic uses calendar-month overflow protection.

The signed raw-body webhook is the only CSRF exemption. Minimal verified event IDs/type/object IDs are persisted before acknowledgement. Queued jobs retry events in `received` or `failed`; they mark `processed` only after successful reconciliation. Canonical sessions, line items, paid amount/currency, expected customer and payment metadata are checked. The return page only displays the authenticated owner's stored state. Duplicate/out-of-order events do not grant extra terms; canonical refunded/disputed payments do not reactivate from late success. A won dispute restores only a remaining original period.

## Withdrawal

Billing, footer/help and purchase-confirmation mail link to `/withdraw`. GET only opens a form. POST records an identifiable notice and receipt time without requiring a reason or login. Authenticated verified ownership is sufficient for ordinary eligible automatic cancellation/refund. Other notices enter review; where the supplied email matches the stored purchase email, the acknowledgement includes an expiring signed mailbox-verification link. Its GET only opens confirmation; its CSRF-protected signed POST verifies ownership and applies the **original receipt time**. It does not restart the clock when email retries or confirmation occur.

Eligibility uses Swedish calendar days in Europe/Stockholm, excluding formation day. A last Saturday/Sunday/public holiday, Midsummer Eve, Christmas Eve or New Year's Eve extends the deadline to the next qualifying weekday. Longer rights, dispatch evidence, missing information and additional markets remain review cases; late notices are retained rather than silently denied. See the primary citations in `source-review.md`. Admin extensions are audited and reevaluate already ownership-verified notices using original receipt times.

Eligible withdrawal ends Premium, stops reminders, revokes sharing, retains Free data, and queues an idempotent refund. Refunds use original payment methods and the entire remaining refundable amount; no usage/admin/Stripe fee deduction. An unpaid open Checkout is expired rather than inventing a refund. A late successful payment after withdrawal is reconciled and refunded. Disputes pause routine refunds to avoid duplicate credit. Partial prior refunds reduce only the remaining amount to return.

Refund labels distinguish requested/pending, succeeded, failed/canceled and requires-action. Acknowledgement is not bank arrival. Durable purchase confirmations include the actual offer, amount/tax, timestamps, activation arrangement, no-renewal terms, frozen policy text and withdrawal-form template. Mail is configured locally as nondelivering `array`; test actual durable delivery with the real provider before selling.

## Required real sandbox exercise

1. Set approved test Price/merchant/tax configuration. Use an authenticated verified account. Confirm unchecked immediate-access behavior and final payment-obligation wording.
2. Run the official Stripe CLI listener: `stripe listen --forward-to localhost:8000/stripe/webhook`; place its endpoint secret in protected `.env`. Start the queue worker. Never paste keys in a report.
3. Complete an actual test card Checkout, inspect the canonical event and stored purchase, confirmation email and Billing. Repeat webhooks and the success URL; unpaid sessions must not activate.
4. Exercise deferred activation, full withdrawal after immediate use, repeated notices, logged-out mailbox verification, pending/failing refunds, an outage/retry, expired Checkout, partial prior refund and disputes. Reconcile with `php artisan swedenstart:reconcile`. Record real event/transaction IDs in protected operational records, not a public document.
5. Confirm current event/API versions and queue/retry handling. Run the MySQL concurrency suite. No assertion here substitutes for these actual external-system tests.

Monitor available refund balance and pending refunds daily while selling; alert before repayment deadlines and resolve insufficient-balance or failed refunds promptly. The processor's settlement timing does not replace merchant obligations. After a definitively failed Stripe refund, do not blindly change idempotency keys: verify canonical state and have an operator use Stripe's documented recovery path, then reconcile. Keep the original notice and due date throughout. Account erasure does not remove this financial obligation.
