Busflow Docs

Internal documentation portal

Skip to content

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 β†’ Payout

Grounded 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 ​

StepActorAction
1OperatorCompletes Nhost signup β†’ Operator.status = ONBOARDING
2SystemCreates Mollie sub-account via Mollie Connect (OAuth)
3MollieReturns organization identifier + onboarding URL
4SystemStores Mollie organization identifier + onboarding status in the operator_integrations table (type = MOLLIE)
5OperatorCompletes Mollie KYC onboarding (identity, bank account, UBO β€” required by EU AML regulations)
6MollieFires 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:

typescript
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:

FieldTypeDescription
deposit_configJSONBOperator-level deposit rules (see Β§3)
final_payment_configJSONBOperator-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:

  1. tour_templates.deposit_config (if set β†’ use this)
  2. operators.deposit_config (tenant default)
  3. System default: [TBD β€” product decision: default deposit percentage, likely governed by Β§ 651a BGB]

JSONB Structure ​

typescript
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 ​

StepEntityStateMollie Action
1CheckoutSessionACTIVEβ€”
2BookingDRAFTβ€”
3Payment (DEPOSIT)PENDINGCreate Mollie payment with deposit amount + marketplace routing
4Passengerβ€”Redirect to Mollie Checkout (hosted payment page)
5Mollie webhookpayment completedNestJS receives callback
6Payment (DEPOSIT)COMPLETEDprovider_transaction_id + payment_method saved
7BookingDEPOSIT_PAIDBookingConfirmed event emitted β†’ CostingSheet locks
8CheckoutSessionCONVERTEDβ€”
9SeatReservationCONFIRMEDβ€”
10TicketconditionalIf 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_PAID as BookingConfirmed trigger. 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:

typescript
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].

StepTriggerAction
1Hasura Scheduled Trigger final_payment_reminderDaily scan: for each booking in DEPOSIT_PAID, checks if start_date - reminder_days_before_start ≀ today
2FinalPaymentDue eventCommunications sends Magic Link via configured channels
3Passenger clicks Magic LinkOpens passenger portal showing outstanding balance
4Payment (FINAL_PAYMENT)PENDING β†’ Mollie Checkout
5Mollie webhook: payment completedPayment.status = COMPLETED
6BookingFULLY_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 OutcomePayment TypePayment StatusBooking StatusSide Effects
Payment succeededDEPOSIT→ COMPLETED→ DEPOSIT_PAIDEmit BookingConfirmed; lock CostingSheet; confirm SeatReservations; issue Tickets (if ticket_issuance_trigger = DEPOSIT_PAID)
Payment succeededFINAL_PAYMENT→ COMPLETED→ FULLY_PAIDUpdate FinancialLedger realized revenue; issue Tickets (if ticket_issuance_trigger = FULLY_PAID)
Payment failedany→ FAILED— (no change)Emit PaymentFailed → Communications sends retry link
Payment expiredany→ FAILED— (no change)If DEPOSIT: release SeatReservations, expire CheckoutSession. If FINAL_PAYMENT: flag for dispatcher review.
Payment canceledany→ FAILED— (no change)Same as expired

Refund Webhooks ​

Refund OutcomePayment StatusBooking StatusSide 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 OutcomeIntegration Impact
Onboarding completedUpdate operator_integrations config: onboarding_status = COMPLETED, enable payment acceptance
Needs additional dataFlag 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 ​

ConcernStrategy
IdempotencyAll webhook handlers are idempotent. provider_transaction_id (unique constraint on payments table) prevents duplicate processing.
Retry deliveryMollie 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).
VerificationHandlers verify payment status via Mollie API before applying state transitions β€” do not trust webhook body alone.
Dead lettersFailed webhook processing follows the error handling pattern in workflow-orchestration.md.

Failure Scenarios ​

ScenarioHandling
Webhook arrives before checkout completes[TBD β€” implementation detail] Queue and process when Booking exists.
Mollie API unavailable at payment creationReturn error to frontend, retry with backoff. [TBD β€” retry strategy]
Double webhook deliveryIdempotency guard on provider_transaction_id
Partial marketplace routing failureLog, 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 ​

OperationKey InputsOutputNotes
Create deposit paymentbooking_id, tenant_id, amount (from DepositConfig Β§3), currencyMollie checkout URLAmount calculated from DepositConfig cascade. Includes marketplace routing (Β§6).
Create final payment linkbooking_id, amount_remaining (= total_amount βˆ’ completed deposits)Checkout URL (sent via Magic Link Β§4.2)Same routing as deposit.
Create partial refundPayment reference, amount, descriptionβ€”Refunds against operator sub-account balance.

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.

OperationKey InputsOutputNotes
Create onboard checkout sessiontenant_id, onboard_sale_id, tour_offering_id, item_description, amount, currency, passenger_contact (phone and/or email)checkout_session_id, checkout_url, expires_atDefault 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 ​

OperationInputOutputSide Effects
Cancel onboard checkoutcheckout_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.

Internal documentation β€” Busflow