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.
Pattern 1: PSP-Managed Hold (Recommended)¶
SCOPE_ITEM: Use Stripe/Mollie payout timing to hold funds.
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?