# Mollie Payment Integration — Design Overview

## 1. Purpose

`PaymentController` is the single integration point between the app and Mollie for subscription-based memberships ("Kulibri Subscription"). It covers:

- Creating a customer + first payment (with mandate consent)
- Verifying payment/subscription status after redirect
- Processing Mollie webhooks (payment + subscription)
- Syncing invoices from Mollie payment history
- Trial memberships (no Mollie involvement)
- Cancellation and access revocation
- Nightly/cron reconciliation of expired subscriptions

It relies on Mollie's **first payment + mandate → recurring subscription** model (see the linked Mollie recurring payments docs at the top of the file), rather than creating a Mollie Subscription object directly at checkout time.

## 2. Core Entities

| Model | Role |
|---|---|
| `User` | Has `customer_id` (Mollie customer), `roles` (bitmask-ish, incremented/decremented for premium access) |
| `Membership` | Plan definition: name, price, vat, interval (e.g. `12M`, `14D`) |
| `Subscription` | Local record of a user's subscription lifecycle; stores `payment_id`, `subscription_id` (Mollie), `status`, `start_date`/`end_date`, raw `payment_response` |
| `Invoice` | One row per successfully paid payment, linked to a `Subscription`, addressable by a generated `key` (public invoice lookup) |

## 3. Payment Creation Flow (`createPayment`)

```mermaid
sequenceDiagram
    participant FE as Frontend
    participant API as PaymentController
    participant DB as MySQL
    participant Mollie as Mollie API

    FE->>API: POST createPayment(user_id)
    API->>DB: find/create Membership (config-driven)
    API->>DB: find User
    alt no customer_id yet
        API->>Mollie: customers.create()
        Mollie-->>API: customer
        API->>DB: save customer_id on User
    end
    API->>DB: check for active Subscription (reject if exists)
    API->>DB: create Subscription (status=null, payment_id=null)
    API->>Mollie: payments.create(sequenceType=first, customerId, webhookUrl, metadata.subscription_id)
    Mollie-->>API: payment
    API->>DB: update Subscription with payment_id/status/description
    API-->>FE: payment (contains Mollie checkout redirect link)
```

Key points:
- Wrapped in a DB transaction; rolled back on any exception.
- Guards against duplicate active subscriptions per user before creating a new one.
- `sequenceType: "first"` is what establishes the mandate for future recurring charges — no Mollie Subscription object is created yet at this step.
- `metadata.subscription_id` is the correlation key used later by the webhook to map a Mollie payment back to the local `Subscription` row.
- Amount formatting (`explode('.', ...)` then re-padding the decimal) is a slightly fragile way of ensuring 2-decimal-place currency values.

## 4. Post-Redirect Status Check (`getPaymentDetailsBySubscriptionID`)

After Mollie redirects the user back to `redirectUrl`, the frontend calls this endpoint to poll/confirm status:

- Delegates the actual status resolution to `CheckAndCreateSubscription::checkStatusAndMakeSubscription` (external class, not shown here) — this is presumably where the Mollie **Subscription** object actually gets created from the mandate once the first payment is confirmed paid.
- Sends `PaymentConfirmationMail` once, guarded by a `mail_sent` flag.
- Computes a `completion_date` from the membership interval.
- Grants premium access by incrementing `user->roles` when status is `Paid` (only if role is currently `0` or `256` — suggesting a bitmask where certain values represent "no premium yet").

## 5. Webhook Processing (`paymentWebhook`)

This is the most important and most defensive part of the integration — it's the source of truth for payment status changes.

```mermaid
flowchart TD
    A[Mollie POSTs payment id] --> B{id present?}
    B -- no --> B1[400 Missing payment ID]
    B -- yes --> C[Fetch payment from Mollie]
    C --> D{Found?}
    D -- no --> D1[404 Payment not found]
    D -- yes --> E[Resolve local Subscription]
    E --> E1[via metadata.subscriptionId / subscription_id]
    E --> E2[fallback: payment.subscriptionId -> Subscription.subscription_id]
    E1 & E2 --> F{Subscription found?}
    F -- no --> F1[404 Subscription not found]
    F -- yes --> G{Duplicate? payment_id already stored}
    G -- yes --> G1[200 Already processed]
    G -- no --> H{status}
    H -- canceled/failed --> I[Update status; cancel if past end_date]
    H -- paid --> J[Fetch customer + their Mollie subscriptions]
    J --> K[Update local end_date from nextPaymentDate]
    K --> L[Create Invoice via CheckAndCreateSubscription::createInvoice]
    L --> M[Grant premium role if currently 0/256]
```

Notable design decisions:
- **Every webhook call is logged to disk** via `Storage::put($timestamp.'.json', $request)` — useful for audit/replay but will accumulate files indefinitely (no cleanup visible).
- **Idempotency**: duplicate webhook deliveries are detected by comparing `subscriptionData->payment_id === $orderId` before reprocessing.
- **Subscription resolution has two paths** (metadata-based, and Mollie-subscriptionId-based) to handle both the first payment (has metadata, no Mollie subscription yet) and recurring payments (has a Mollie subscription, metadata may not carry through).
- Failure handling distinguishes Mollie API errors (`\Mollie\Api\Exceptions\ApiException`) from generic exceptions, and both trigger an **error email** to a hardcoded list of recipients (`sendErrorEmail`).
- On cancel/fail, it only actually cancels the subscription (`CheckAndCreateSubscription::cancelSubscriptionAction`) if `end_date` has already passed — otherwise it just records the failed status and leaves access intact until expiry.
- There's a large **commented-out "v2" webhook flow** at the bottom of the method that appears to be an earlier design where the Mollie Subscription object was created *inside* the webhook (mandate-driven), rather than by `CheckAndCreateSubscription`. This is dead code that should probably be removed once the current flow is confirmed stable.

## 6. Invoice Reconciliation (`createInvoiceInSyncWithMollie`)

A batch/cron-style endpoint that re-syncs local `Invoice` records against Mollie's payment history for every subscription that has a Mollie `subscription_id`:

- For each local subscription with a `subscription_id`, fetches the Mollie customer and their payments filtered by that subscription.
- For the most recent payment, refreshes the local `payment_id`.
- For any `paid` payment without a matching local `Invoice`, creates one (computing `invoice_end_date` via `CheckAndCreateSubscription::addValuesInDate`).
- Cleans up orphaned invoices (`payment_id IS NULL`) for that subscription.
- Continues past per-subscription errors, collecting them into `syncErrors` rather than failing the whole batch — but still wraps everything in one DB transaction, so a late failure elsewhere would roll back earlier successful inserts within the same request (the `continue` only skips to the next subscription's *Mollie calls*, not a per-subscription DB commit).

This looks like a safety-net job to catch payments that a missed/late webhook didn't record.

## 7. Trial Flow (`startTrial` / `deleteTrial`)

Trials are **entirely local** — no Mollie API calls:
- Looks up (or creates) a `Membership` with `interval = "14D"`, price `0`.
- Creates a `Subscription` with `status = Paid`, `start_date = now`, `end_date = now + 14 days`, `has_trial_start = true`.
- Immediately creates an `Invoice` for it (via the same `CheckAndCreateSubscription::createInvoice` helper used by paid webhooks).
- `deleteTrial` sets `cancel_status = Canceled` and decrements the user's role bit (with a slightly odd bounds check: `roles > 0 && roles < 255 || roles > 256`).

## 8. Cancellation (`cancelSubscription`)

Thin wrapper delegating to `CheckAndCreateSubscription::cancelSubscriptionAction($mollie, $user_id)` — the actual Mollie-side cancellation and role revocation logic lives outside this controller.

## 9. Scheduled Expiry Sweep (`updateLatestPaymentStatus`)

Iterates all "active" subscriptions (via `getAllActiveSubscription()`), and for each:
- Revokes the user's premium role unconditionally.
- Fetches the customer's payments from Mollie and inspects the **last** payment in the list.
- Marks the local subscription `Expired`, stores the latest payment status/response.

This appears to run on a schedule for subscriptions whose end date has passed but whose cancellation wasn't caught by the webhook — a second safety net alongside the invoice sync job.

## 10. Cross-Cutting Observations

- **Role management as access control**: premium/free access is entirely driven by incrementing/decrementing `user->roles`, checked inline in at least four places (`getPaymentDetailsBySubscriptionID`, `paymentWebhook`, `deleteTrial`, `updateLatestPaymentStatus`) with slightly different guard conditions each time. This logic is a good candidate to centralize (e.g., `User::grantPremium()` / `revokePremium()`) to avoid drift.
- **Metadata as the correlation key**: the `subscription_id` in Mollie payment metadata is the glue between Mollie and the local DB for first payments; recurring payments rely on Mollie's own `subscriptionId` instead. Both paths are handled in the webhook but not elsewhere (e.g., `getSubscriptionDetail` has no such fallback).
- **Hardcoded error-notification recipients** (`andreas.kutzer@gmail.com`, cc'd Gmail addresses) in `sendErrorEmail` — likely worth moving to config.
- **Debug/dead code**: `getSubscriptionDetail` is essentially a scratch/debug endpoint (hardcoded payment ID, large commented-out block reconstructing a fake Mollie response). Not part of the real integration surface.
- **No explicit webhook signature verification** is visible in `paymentWebhook` — it trusts the `id` in the POST body and re-fetches the payment from Mollie by ID to verify authenticity indirectly (which is actually the correct/recommended Mollie pattern — the webhook body is untrusted, only used to know *which* payment to re-fetch).
- **Duplicate email logic**: `sentPaymentConfirmationMail` and `getPaymentDetailsBySubscriptionID` both send `PaymentConfirmationMail` independently rather than sharing a helper.

## 11. Suggested Follow-Ups (for your design doc, if useful)

- Document `CheckAndCreateSubscription` as its own component — it's referenced constantly here but its internals (status polling, subscription creation, invoice creation, cancellation, interval math) are the actual core business logic.
- Clarify whether `getSubscriptionDetail` should be removed before this is finalized as "current design."
- Decide on a single source of truth for role grant/revoke to reduce the four near-duplicate implementations.