Mollie Integration Specification β
This document specifies the Mollie Marketplaces integration for operator-to-passenger payment processing within the BusFlow platform.
Related docs: PRODUCT_payments-and-billing.md Β· schema-commerce.md Β§payments Β· ADR-009
WARNING
Review status: This spec was initially drafted during the L2 Checkout & Payment review. Sections marked [TBD β product decision] require explicit sign-off before implementation. Mollie API details (event names, endpoints, OAuth scopes) illustrate the process based on Mollie's public documentation; validate them at implementation time.
1. Architecture Overview β
BusFlow uses Mollie Marketplaces to route passenger payments to individual operator sub-accounts. The platform acts as the marketplace owner (collecting platform fees), while each Operator tenant has a linked Mollie sub-account which receives payouts.
Passenger β Mollie Checkout β Platform Fee Split β Operator Sub-Account β PayoutGrounded in: PRODUCT_payments-and-billing.md Β§1 β "Mollie provides a high trust factor in Europe, natively supporting DACH payment methods. Its Marketplace routing allows for delayed payouts (acting as an escrow for bus trips)."
2. Sub-Account Provisioning (Operator Onboarding) β
Each Operator requires a Mollie sub-account before accepting payments. Mollie-specific configuration resides in the operator_integrations table (not directly on operators), scoped by integration type. See schema-backoffice.md Β§operator_integrations.
Flow β
| Step | Actor | Action |
|---|---|---|
| 1 | Operator | Completes Nhost signup β Operator.status = ONBOARDING |
| 2 | System | Creates Mollie sub-account via Mollie Connect (OAuth) |
| 3 | Mollie | Returns organization identifier + onboarding URL |
| 4 | System | Stores Mollie organization identifier + onboarding status in the operator_integrations table (type = MOLLIE) |
| 5 | Operator | Completes Mollie KYC onboarding (identity, bank account, UBO β required by EU AML regulations) |
| 6 | Mollie | Fires onboarding webhook β System updates integration config β Operator ready to accept payments |
NOTE
[TBD β implementation detail] The team must validate the exact Mollie Connect API flow (OAuth scopes, redirect URLs, onboarding webhook events) against Mollie documentation during implementation.
2.1 Integration Config (Mollie) β
The system stores Mollie-specific fields in the operator_integrations table under type = 'MOLLIE' with a JSONB config column:
interface MollieIntegrationConfig {
organization_id: string; // Mollie sub-account ID
onboarding_status: 'PENDING' | 'COMPLETED' | 'NEEDS_DATA';
// Additional Mollie-specific fields as needed at implementation
}2.2 Operator Payment Config β
The following fields remain on the operators table (or in operator-level config), as they are platform concerns β not Mollie-specific:
| Field | Type | Description |
|---|---|---|
deposit_config | JSONB | Operator-level deposit rules (see Β§3) |
final_payment_config | JSONB | Operator-level final payment timing (see Β§4.2) |
NOTE
deposit_config and final_payment_config are tenant-level defaults. Operators can override both per TourTemplate.
3. Deposit Configuration β
The system resolves the deposit percentage via a cascading lookup:
tour_templates.deposit_config(if set β use this)operators.deposit_config(tenant default)- System default:
[TBD β product decision: default deposit percentage, likely governed by Β§ 651a BGB]
JSONB Structure β
interface DepositConfig {
percentage: number; // e.g., 20 (= 20%)
type: 'PERCENTAGE' | 'FIXED';
min_amount: number | null; // Optional floor
}IMPORTANT
[TBD β product decision] The system default deposit percentage (fallback when neither template nor operator defines one) must be a conscious product decision. Β§ 651a BGB governs travel package advance payment limits β legal review required.
4. Checkout-to-Payment Pipeline β
4.1 B2C Payment Flow β
| Step | Entity | State | Mollie Action |
|---|---|---|---|
| 1 | CheckoutSession | ACTIVE | β |
| 2 | Booking | DRAFT | β |
| 3 | Payment (DEPOSIT) | PENDING | Create Mollie payment with deposit amount + marketplace routing |
| 4 | Passenger | β | Redirect to Mollie Checkout (hosted payment page) |
| 5 | Mollie webhook | payment completed | NestJS receives callback |
| 6 | Payment (DEPOSIT) | COMPLETED | provider_transaction_id + payment_method saved |
| 7 | Booking | DEPOSIT_PAID | BookingConfirmed event emitted β CostingSheet locks |
| 8 | CheckoutSession | CONVERTED | β |
| 9 | SeatReservation | CONFIRMED | β |
| 10 | Ticket | conditional | If ticket_issuance_trigger = DEPOSIT_PAID (operator default): ticket issued now. If FULLY_PAID: deferred to final payment. See ADR-014. |
Grounded in: ADR-009 β
DEPOSIT_PAIDasBookingConfirmedtrigger. Full booking lifecycle in booking-lifecycle-protocol.md.
4.2 Final Payment Flow β
The final payment reminder timing is operator-configurable via final_payment_config:
interface FinalPaymentConfig {
reminder_days_before_start: number; // e.g., 30 β first reminder at start_date - N days
escalation_days_before_start: number; // e.g., 14 β second reminder / escalation
flag_days_before_start: number; // e.g., 7 β flag for dispatcher review
}Resolution cascade: tour_templates.final_payment_config β operators.final_payment_config β system default [TBD].
| Step | Trigger | Action |
|---|---|---|
| 1 | Hasura Scheduled Trigger final_payment_reminder | Daily scan: for each booking in DEPOSIT_PAID, checks if start_date - reminder_days_before_start β€ today |
| 2 | FinalPaymentDue event | Communications sends Magic Link via configured channels |
| 3 | Passenger clicks Magic Link | Opens passenger portal showing outstanding balance |
| 4 | Payment (FINAL_PAYMENT) | PENDING β Mollie Checkout |
| 5 | Mollie webhook: payment completed | Payment.status = COMPLETED |
| 6 | Booking | FULLY_PAID |
IMPORTANT
If the system does not receive the final payment by escalation_days_before_start, it sends a second reminder. If it still does not receive the payment by flag_days_before_start, it flags the booking for dispatcher review (manual follow-up or cancellation). We do not implement automatic cancellation in V1.
4.3 B2B Payment Flow β
NOTE
[TBD β product decision] B2B agencies may bypass B2C deposit rules and book on agency invoice (see booking-widget/user-journeys.md Journey 3). The B2B payment flow, commission handling, and invoice-based booking mechanism are out of scope for this spec and Management will define them separately.
5. Webhook β State Transition Mapping β
A dedicated NestJS webhook handler receives all Mollie webhooks.
NOTE
The Mollie event names below illustrate the process based on Mollie's webhook documentation. The team must validate the actual event names, payload structure, and delivery guarantees against the Mollie API at implementation time. The state transitions (which Booking/Payment status follows which payment outcome) constitute the authoritative part of this table.
NOTE
For the complete processPaymentWebhook NestJS handler contract (input, guards, idempotency, cascade sequences), see booking-lifecycle-protocol.md Β§processPaymentWebhook.
Payment Webhooks β
| Payment Outcome | Payment Type | Payment Status | Booking Status | Side Effects |
|---|---|---|---|---|
| Payment succeeded | DEPOSIT | β COMPLETED | β DEPOSIT_PAID | Emit BookingConfirmed; lock CostingSheet; confirm SeatReservations; issue Tickets (if ticket_issuance_trigger = DEPOSIT_PAID) |
| Payment succeeded | FINAL_PAYMENT | β COMPLETED | β FULLY_PAID | Update FinancialLedger realized revenue; issue Tickets (if ticket_issuance_trigger = FULLY_PAID) |
| Payment failed | any | β FAILED | β (no change) | Emit PaymentFailed β Communications sends retry link |
| Payment expired | any | β FAILED | β (no change) | If DEPOSIT: release SeatReservations, expire CheckoutSession. If FINAL_PAYMENT: flag for dispatcher review. |
| Payment canceled | any | β FAILED | β (no change) | Same as expired |
Refund Webhooks β
| Refund Outcome | Payment Status | Booking Status | Side Effects |
|---|---|---|---|
| Refund settled | β REFUNDED | β REFUNDED (if full) / unchanged (if partial) | Create Payment row with type = REFUND or PARTIAL_REFUND, negative amount. The operator's Mollie sub-account balance funds the refunds. Update FinancialLedger. |
Onboarding Webhooks β
| Onboarding Outcome | Integration Impact |
|---|---|
| Onboarding completed | Update operator_integrations config: onboarding_status = COMPLETED, enable payment acceptance |
| Needs additional data | Flag operator for data completion |
6. Marketplace Routing (Split Payments) β
Every payment call includes Mollie Marketplace routing to split funds between the BusFlow platform and the operator sub-account.
IMPORTANT
[TBD β product decision] Platform fee structure: flat percentage per transaction? Tiered by operator volume? Per-operator negotiated rate? This is a core business model decision. The routing mechanism supports any of these β the fee calculation logic is the product decision.
WARNING
[TBD β product decision] Platform fee calculation source: Every payment operation (Β§8) requires a platform fee value for marketplace routing. No schema or config table currently stores this value. Options: (1) column on operators (per-operator negotiated), (2) global platform config, (3) Lago-driven metered billing sync. We must resolve this before we can implement payment routing.
BusFlow does not manage payout timing directly. Operators configure their payout preferences in their Mollie dashboard (linked from the BusFlow backoffice).
7. Error Handling & Idempotency β
| Concern | Strategy |
|---|---|
| Idempotency | All webhook handlers are idempotent. provider_transaction_id (unique constraint on payments table) prevents duplicate processing. |
| Retry delivery | Mollie retries webhook delivery with exponential backoff over ~8 hours (per Mollie documentation). A daily payment_reconciliation_sweep Scheduled Trigger catches any missed webhooks beyond this window (see booking-lifecycle-protocol.md Β§payment_reconciliation_sweep). |
| Verification | Handlers verify payment status via Mollie API before applying state transitions β do not trust webhook body alone. |
| Dead letters | Failed webhook processing follows the error handling pattern in workflow-orchestration.md. |
Failure Scenarios β
| Scenario | Handling |
|---|---|
| Webhook arrives before checkout completes | [TBD β implementation detail] Queue and process when Booking exists. |
| Mollie API unavailable at payment creation | Return error to frontend, retry with backoff. [TBD β retry strategy] |
| Double webhook delivery | Idempotency guard on provider_transaction_id |
| Partial marketplace routing failure | Log, alert ops, manual reconciliation via Mollie dashboard |
8. Payment Service Contracts β
Service-level operations for payment creation. All operations include Mollie Marketplace routing (Β§6) and the payment_type discriminator in Mollie metadata for webhook routing. For the webhook handler contract, see booking-lifecycle-protocol.md Β§processPaymentWebhook.
Booking Payment Operations β
| Operation | Key Inputs | Output | Notes |
|---|---|---|---|
| Create deposit payment | booking_id, tenant_id, amount (from DepositConfig Β§3), currency | Mollie checkout URL | Amount calculated from DepositConfig cascade. Includes marketplace routing (Β§6). |
| Create final payment link | booking_id, amount_remaining (= total_amount β completed deposits) | Checkout URL (sent via Magic Link Β§4.2) | Same routing as deposit. |
| Create partial refund | Payment reference, amount, description | β | Refunds against operator sub-account balance. |
8.1 Onboard Payment Link Contracts β
The following contracts support the OnboardSale β Commerce Payment Link Bridge flow. The Operations sync handler calls the onboard checkout operation when processing an OnboardSale with payment_method = PAYMENT_LINK. See schema-operations.md Β§onboard_sales for the full sync handler flow and edge states.
| Operation | Key Inputs | Output | Notes |
|---|---|---|---|
| Create onboard checkout session | tenant_id, onboard_sale_id, tour_offering_id, item_description, amount, currency, passenger_contact (phone and/or email) | checkout_session_id, checkout_url, expires_at | Default expiry: 60 min. Operator-configurable via operator_settings.onboard_payment_link_ttl_minutes (default: 60, min: 15, max: 180). Idempotent on onboard_sale_id β returns existing non-expired session if one exists (crash-gap recovery, see schema-operations.md Β§onboard_sales). |
Passenger contact is passed as transient input (not persisted on OnboardSale). Primary: phone (WhatsApp/SMS). Fallback: email.
Cancellation Contract β
| Operation | Input | Output | Side Effects |
|---|---|---|---|
| Cancel onboard checkout | checkout_session_id, tenant_id, reason | β | Expires session, cancels Mollie payment. If already paid β initiates refund. |
Called by the Operations void handler when the driver voids an OnboardSale.
NOTE
Uses the standard Mollie Payments API (/v2/payments) β consistent with the booking checkout pattern. The checkout_url from the response is shareable via WhatsApp/SMS. Alternative: Mollie Payment Links API (/v2/payment-links) generates dedicated shareable URLs with built-in expiry β consider if the standard checkout_url expiry behavior proves insufficient.
NOTE
Passenger contact resolution: Primary path: ticket_id β commerce.tickets β passengers (email/phone) for upsells to existing passengers. Fallback: driver-entered phone number (transient input passed in passenger_contact, not persisted on OnboardSale) for non-ticketed sales.