Skip to content

Transactions & Payments

SCOPE_ITEM: End-to-end transaction processing for marketplace platforms including payment collection, escrow/hold, split payments, platform fees, seller payouts, refunds, and dispute resolution.

Payment Provider Decision

IF: Marketplace is Netherlands/EU focused, iDEAL is primary payment method. THEN: Use Mollie Connect (NL, PRIMARY).

IF: Marketplace is global or USD-first. THEN: Use Stripe Connect (US, secondary — EU data sovereignty risk. Discuss with client.).

IF: Marketplace needs both EU and global coverage. THEN: Mollie Connect primary for EU + Stripe Connect secondary for non-EU (NOTE: Stripe is US-based — sovereignty risk).

Criteria Mollie Connect (NL — PRIMARY) Stripe Connect (US — SECONDARY)
iDEAL support Native, optimised Yes (via Stripe)
Split payments Yes (route payments) Yes (destination/separate charges)
Escrow/hold Yes (delayed payouts, max 90 days) Yes (manual payout timing)
Seller onboarding KYC Yes (OAuth onboarding) Yes (Express/Custom accounts)
Currencies EUR, GBP primarily 135+ currencies
Payout speed Next business day (NL) 2-7 business days (standard)
Platform fee model Split routing with platform share application_fee_amount
Base fee (EU cards) 1.8% + EUR 0.25 (variable) 1.5% + EUR 0.25
Instant payouts Not available Yes (1% fee, capped EUR 5)
API quality Good, some gaps Excellent, comprehensive
Data sovereignty EU (NL-headquartered) US-based — sovereignty risk

Escrow Patterns

SCOPE_ITEM: Hold buyer funds until delivery/completion is confirmed before releasing to seller.

SCOPE_ITEM: Use Stripe/Mollie payout timing to hold funds.

Buyer pays → Funds held at PSP → Delivery confirmed → Payout to seller
                                                    ↘ Dispute → Admin decides

INCLUDES: - Payment captured immediately (buyer charged). - Funds held in PSP balance (not transferred to seller yet). - Release trigger: buyer confirms receipt OR auto-release after N days. - Dispute window before auto-release.

Stripe Connect Implementation

1. Create PaymentIntent with transfer_data.destination = seller_account_id
   - capture_method: "automatic" (charge immediately)
   - transfer_group: order_id (for reconciliation)
   - application_fee_amount: platform_fee_in_cents

2. Stripe holds funds in platform balance
   - Seller payout not yet created

3. On delivery confirmation:
   - Create Transfer to seller Connect account
   - Stripe schedules payout to seller bank account

4. On dispute:
   - Pause transfer creation
   - Admin resolves → transfer to seller OR refund to buyer

CHECK: With separate charges and transfers, platform has full control over when to transfer funds to seller. CHECK: Stripe requires application_fee_amount in smallest currency unit (cents).

Mollie Connect Implementation

1. Create Payment with split routing:
   - Route to seller organisation with "delay" flag
   - Platform share routed to platform balance

2. Mollie holds seller portion in platform balance
   - Delayed payouts: up to 90 days

3. On delivery confirmation:
   - Release payment to seller via Mollie API
   - Mollie schedules payout to seller bank

4. On dispute:
   - Keep funds delayed
   - Admin resolves → release or refund

CHECK: Mollie delayed payouts max 90 days — for longer holds, contact Mollie. CHECK: Mollie split payments support EUR and GBP only. CHECK: Use reverseRouting: true for refunds to auto-debit seller balances.

Pattern 2: Milestone-Based Release (Services)

OPTIONAL: SCOPE_ITEM: Release funds in stages as service milestones are completed.

INCLUDES: - Define milestones at order creation (e.g., 30% upfront, 40% mid, 30% final). - Each milestone has acceptance criteria. - Buyer confirms milestone completion → partial transfer. - Dispute possible per milestone.

CHECK: Milestone-based release adds significant complexity. Only include for service marketplaces with high-value transactions.


Split Payments

SCOPE_ITEM: Automatically divide payment between platform and seller.

Commission Model

INCLUDES: - Platform commission as percentage of transaction amount. - Commission deducted before seller receives funds. - Commission rate configurable per seller (default + overrides). - Commission displayed in order confirmation (transparency).

Fee Structure Data Model

CREATE TABLE platform_fee_config (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,                    -- "default", "trusted_seller", "top_seller"
  commission_rate NUMERIC(5,4) NOT NULL, -- 0.1000 = 10%
  fixed_fee_cents INTEGER DEFAULT 0,     -- optional fixed fee per transaction
  min_fee_cents INTEGER DEFAULT 0,       -- minimum fee floor
  is_default BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE transactions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id UUID REFERENCES orders(id),
  buyer_id UUID REFERENCES users(id),
  seller_id UUID REFERENCES users(id),
  amount_cents INTEGER NOT NULL,           -- total buyer payment
  currency TEXT DEFAULT 'EUR',
  platform_fee_cents INTEGER NOT NULL,     -- platform commission
  seller_amount_cents INTEGER NOT NULL,    -- amount for seller
  psp_fee_cents INTEGER,                   -- Stripe/Mollie processing fee
  psp_payment_id TEXT NOT NULL,            -- Stripe PaymentIntent or Mollie Payment ID
  psp_transfer_id TEXT,                    -- Stripe Transfer or Mollie route ID
  status TEXT DEFAULT 'pending',           -- pending, paid, held, transferred, refunded, disputed
  held_until TIMESTAMPTZ,                  -- auto-release date
  transferred_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

Tiered Commission

OPTIONAL: SCOPE_ITEM: Lower commission rates for higher-performing sellers.

Tier Monthly GMV Commission
Standard EUR 0 - 5,000 12%
Growth EUR 5,001 - 20,000 10%
Professional EUR 20,001 - 100,000 8%
Enterprise EUR 100,001+ Custom (negotiated)

INCLUDES: - Automatic tier calculation (monthly GMV rolling window). - Tier upgrade notification to seller. - Tier downgrade with grace period (1 month).


Checkout Flow

SCOPE_ITEM: Buyer purchase experience from cart to confirmation.

Standard Flow

1. Add to cart (or "Buy Now" direct purchase)
   └── Cart stored server-side (authenticated) or localStorage (guest)

2. Checkout page
   ├── Order summary (items, quantities, prices, shipping)
   ├── Shipping address (saved or new)
   ├── Payment method selection
   │     ├── iDEAL (bank selection)
   │     ├── Credit/debit card (Stripe Elements or Mollie Components)
   │     ├── Other methods as configured
   └── "Place Order" button

3. Payment processing
   ├── Create PSP payment/intent
   ├── 3D Secure challenge (if required by bank)
   ├── Payment confirmation or failure
   └── Redirect to order confirmation page

4. Post-payment
   ├── Order confirmation email to buyer
   ├── New order notification to seller
   ├── Order status: "Paid — Awaiting Shipment"
   └── Funds held in escrow (if enabled)

Multi-Seller Cart

OPTIONAL: SCOPE_ITEM: Cart with items from multiple sellers, split into sub-orders.

INCLUDES: - Single checkout for items from multiple sellers. - One payment from buyer, split into multiple transfers. - Separate order per seller (independent fulfilment). - Separate shipping per seller.

CHECK: Multi-seller cart requires "separate charges and transfers" on Stripe or multi-route split payments on Mollie. CHECK: Shipping calculation per seller adds complexity — confirm needed.

Guest Checkout

OPTIONAL: SCOPE_ITEM: Purchase without creating an account.

INCLUDES: - Email address required (for order updates). - Post-purchase account creation prompt. - Guest orders linked to account if same email registers later.


Refunds

SCOPE_ITEM: Process buyer refund requests.

Refund Types

Type Trigger Platform Fee PSP Fee
Full refund Buyer request, admin decision Returned to buyer NOT refunded by Stripe/Mollie
Partial refund Partial issue (damaged item) Proportionally returned NOT refunded
Seller-initiated Seller cancels, out of stock Returned to buyer NOT refunded

CHECK: PSP processing fees are NOT refunded on refund — platform absorbs this cost. CHECK: Platform commission refund policy must be defined: - Option A: Refund commission to buyer (platform loses fee). - Option B: Deduct commission from seller balance (seller absorbs). - Recommendation: Option A for buyer-initiated, Option B for seller fault.

Refund Flow

1. Buyer requests refund (within return window)
   └── Reason selection + optional evidence

2. Seller notified, can accept or dispute
   ├── Accept → refund processed automatically
   └── Dispute → escalated to admin

3. Admin reviews (if disputed)
   └── Decision: approve refund, partial refund, or deny

4. Refund processed via PSP API
   └── Stripe: create Refund on PaymentIntent
   └── Mollie: create Refund on Payment (reverseRouting: true)

5. Notifications sent to both parties

Refund Data Model

CREATE TABLE refunds (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  transaction_id UUID REFERENCES transactions(id),
  requested_by UUID REFERENCES users(id),
  reason TEXT NOT NULL,
  amount_cents INTEGER NOT NULL,
  status TEXT DEFAULT 'requested',  -- requested, approved, processed, denied
  psp_refund_id TEXT,
  decided_by UUID REFERENCES users(id),
  decision_reason TEXT,
  processed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now()
);

Dispute Resolution

SCOPE_ITEM: Handle conflicts between buyers and sellers.

Dispute Flow

1. Buyer opens dispute (within dispute window, e.g., 30 days)
   ├── Dispute reason (not received, not as described, damaged)
   ├── Evidence upload (photos, screenshots, tracking info)
   └── Funds held (transfer to seller paused)

2. Seller notified — 72h to respond
   ├── Accept dispute → refund processed
   ├── Counter with evidence → escalate to admin
   └── No response in 72h → auto-resolve in buyer favour

3. Admin review (if contested)
   ├── Review evidence from both parties
   ├── Request additional information (optional)
   └── Decision: full refund, partial refund, deny (release to seller)

4. Resolution
   ├── Refund buyer → Stripe/Mollie refund API
   ├── OR release to seller → create transfer/release payout
   └── Both parties notified of outcome

5. Post-resolution
   ├── Dispute rate updated for seller trust score
   └── Pattern flagging if seller has high dispute rate

Dispute Data Model

CREATE TABLE disputes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  transaction_id UUID REFERENCES transactions(id),
  opened_by UUID REFERENCES users(id),
  reason TEXT NOT NULL,
  status TEXT DEFAULT 'open',  -- open, seller_response, admin_review, resolved
  resolution TEXT,             -- refund_full, refund_partial, denied
  resolution_amount_cents INTEGER,
  resolved_by UUID REFERENCES users(id),
  resolved_at TIMESTAMPTZ,
  seller_response_deadline TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE dispute_evidence (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  dispute_id UUID REFERENCES disputes(id),
  submitted_by UUID REFERENCES users(id),
  type TEXT NOT NULL,          -- text, image, document, tracking_info
  content TEXT,
  file_url TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
);

Payout Scheduling

SCOPE_ITEM: Transfer earned funds to seller bank accounts.

Payout Strategies

Strategy Frequency Use Case
Automatic (PSP default) Daily/weekly (PSP setting) Standard, most marketplaces
On-demand Seller triggers Low-volume sellers want control
Threshold-based When balance exceeds EUR X Reduce payout count/fees
After hold period N days after delivery confirmed Escrow model

INCLUDES: - Payout history visible to seller (amount, date, status, line items). - Payout notification email on transfer. - Payout failure handling (invalid IBAN, bank rejection → notify seller).

Payout Reconciliation

INCLUDES: - Daily reconciliation job (BullMQ scheduled): Match PSP payout events with platform transaction records. - Flag discrepancies for admin review. - Monthly settlement report per seller.


Webhook Handling

SCOPE_ITEM: Process PSP webhook events reliably.

INCLUDES: - Webhook endpoint (/api/webhooks/stripe or /api/webhooks/mollie). - Signature verification (Stripe: stripe-signature header, HMAC; Mollie: webhook URL with payment ID, verify via API callback). - Idempotent processing (store processed event IDs, skip duplicates). - Event queue (BullMQ) for async processing with retry. - Event types handled: - payment_intent.succeeded / payment status paid - payment_intent.payment_failed / payment status failed - charge.refunded / refund status changes - account.updated (Stripe Connect verification changes) - payout.paid / payout.failed

CHECK: Webhook endpoint must return 200 quickly (within 5 seconds). Offload processing to BullMQ queue. CHECK: Webhook endpoint must be idempotent — same event may be delivered multiple times. CHECK: Stripe webhooks must verify signature before processing. CHECK: Mollie webhooks must call back to Mollie API to get payment status (Mollie sends only payment ID in webhook, not status).


Scoping Questions

CHECK: Which PSP (Stripe Connect or Mollie Connect)? CHECK: What payment methods are required (iDEAL, cards, PayPal, Klarna)? CHECK: Is escrow/delayed payout needed? What is the hold period? CHECK: What is the platform commission rate? CHECK: Are tiered commission rates needed? CHECK: Multi-seller cart (one checkout, multiple sellers)? CHECK: What is the refund policy (window, who pays)? CHECK: Is dispute resolution needed? What is the resolution process? CHECK: What is the payout frequency (daily, weekly, on-demand)? CHECK: Does the platform need to handle VAT collection?