13 KiB
Capability Spec Delta: payment-provider-mollie (add-payment-provider-mollie)
Package: @suaveplan/payment-provider-mollie
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. The package is fresh ongenesis, so this delta uses only## ADDED Requirements.
ADDED Requirements
Requirement: MollieProvider implements PaymentProvider
The package SHALL export a MollieProvider class that implements the
PaymentProvider interface from @suaveplan/payment. MollieProvider.name
SHALL equal the string literal "mollie". The class SHALL accept an
ApiKeyConfig (from @suaveplan/payment-providers) in its constructor,
storing the resolved apiKey, baseUrl (default
https://api.mollie.com/v2), timeoutMs (default 30_000), and optional
fetch / signal references for forwarding into the slim-core
httpRequest helper. Every outbound request SHALL carry the header
Authorization: Bearer <apiKey>.
Scenario: provider name is "mollie"
- WHEN
new MollieProvider({ apiKey: "test_x" }).nameis read - THEN the value is the string
"mollie"
Scenario: bearer token surfaced on every call
- WHEN any provider method dispatches a request through the injected
FetchLike - THEN the recorded
init.headers.Authorizationequals"Bearer <apiKey>"
Verified by: src/mollie/mollie.test.ts::"exposes the literal provider name" AND src/mollie/mollie.test.ts::"sends Authorization: Bearer header on every request"
Requirement: createIntent posts to /payments with Mollie amount shape
MollieProvider.createIntent(input) SHALL POST to
${baseUrl}/payments with a JSON body that contains amount: { currency, value } where value is the result of formatMollieAmount(input.amount.amount).
When input.description is undefined, the body SHALL default
description to the string "Payment". When input.returnUrl is
undefined, the body SHALL default redirectUrl to the string
"https://localhost/return". When input.metadata is provided, the
body SHALL include metadata verbatim. The response SHALL be parsed
into a PaymentIntent via mapMollieStatus for status and
parseMollieAmount for amount.value.
Scenario: amount marshalled to two-decimal string
- WHEN
createIntent({ amount: { amount: 5000n, currency: "EUR" } })is invoked - THEN the recorded request body's
amount.value === "50.00"andamount.currency === "EUR"
Scenario: defaults applied when description / returnUrl omitted
- WHEN
createIntent({ amount: { amount: 1000n, currency: "EUR" } })is invoked with nodescriptionorreturnUrl - THEN the recorded request body's
description === "Payment"andredirectUrl === "https://localhost/return"
Verified by: src/mollie/mollie.test.ts::"marshals amount to two-decimal string on createIntent" AND src/mollie/mollie.test.ts::"applies default description and redirectUrl when omitted" AND src/mollie/mollie.test.ts::"forwards metadata on createIntent"
Requirement: confirmIntent and captureIntent are read-throughs
MollieProvider.confirmIntent(intentId) and
MollieProvider.captureIntent(intentId) SHALL both delegate to
getIntent(intentId). Mollie auto-captures, so there is no separate
confirm / capture step at the protocol level.
Scenario: confirm dispatches a GET to /payments/{id}
- WHEN
confirmIntent("tr_123")is invoked - THEN exactly one request is recorded with method
GETand URL ending/payments/tr_123
Scenario: capture dispatches a GET to /payments/{id}
- WHEN
captureIntent("tr_123")is invoked on a Mollie payment with statuspaid - THEN the returned intent's
status === "captured"
Verified by: src/mollie/mollie.test.ts::"confirmIntent reads through to getIntent" AND src/mollie/mollie.test.ts::"captureIntent reads through to getIntent"
Requirement: cancelIntent issues HTTP DELETE
MollieProvider.cancelIntent(intentId) SHALL dispatch an HTTP DELETE
request to ${baseUrl}/payments/${intentId} and parse the response
through the standard payment-mapping pipeline.
Scenario: DELETE dispatched on cancel
- WHEN
cancelIntent("tr_123")is invoked and Mollie returns statuscanceled - THEN the recorded request method is
DELETEand the returned intent'sstatus === "canceled"
Verified by: src/mollie/mollie.test.ts::"cancelIntent issues DELETE and maps canceled status"
Requirement: refund posts to /payments/{id}/refunds
MollieProvider.refund(input) SHALL POST to
${baseUrl}/payments/${input.paymentIntentId}/refunds. When
input.amount is provided, the body SHALL include
amount: { currency, value: formatMollieAmount(input.amount.amount) };
when omitted, the body SHALL be an empty object so Mollie issues a
full refund. The response SHALL be mapped to a Refund with
status resolved through mapMollieRefundStatus and amount.amount
parsed via parseMollieAmount.
Scenario: refund with explicit amount
- WHEN
refund({ paymentIntentId: "tr_123", amount: { amount: 2000n, currency: "EUR" } })is invoked - THEN the recorded request body's
amount.value === "20.00"and the returned refund'samount.amount === 2000n
Scenario: refund without explicit amount
- WHEN
refund({ paymentIntentId: "tr_123" })is invoked - THEN the recorded request body is
{}(noamountfield)
Verified by: src/mollie/mollie.test.ts::"refund posts with explicit amount and round-trips through parseMollieAmount" AND src/mollie/mollie.test.ts::"refund without explicit amount posts empty body"
Requirement: getIntent fetches and maps payment + payment method
MollieProvider.getIntent(intentId) SHALL GET
${baseUrl}/payments/${intentId} and return a PaymentIntent. When
the Mollie response carries method === "creditcard", the resulting
paymentMethod.type SHALL be "card" and paymentMethod.brand /
paymentMethod.last4 SHALL be extracted from details.cardLabel
(lowercased) and the last four characters of details.cardNumber. When
method is any other defined string, the resulting
paymentMethod.type SHALL be "wallet". When method is undefined,
paymentMethod SHALL be undefined.
Scenario: card payment carries brand and last4
- WHEN
getIntent("tr_123")returns a Mollie payment withmethod: "creditcard",details: { cardNumber: "************4242", cardLabel: "Visa" } - THEN the resulting
paymentMethod.type === "card",paymentMethod.brand === "visa",paymentMethod.last4 === "4242"
Scenario: non-card method becomes wallet without last4
- WHEN
getIntent("tr_123")returns a Mollie payment withmethod: "ideal"and nodetails - THEN the resulting
paymentMethod.type === "wallet"andpaymentMethod.last4 === undefined
Scenario: missing method yields undefined paymentMethod
- WHEN
getIntent("tr_123")returns a Mollie payment withmethod: undefined - THEN the resulting
paymentMethod === undefined
Verified by: src/mollie/mollie.test.ts::"getIntent maps a creditcard payment to card with brand and last4" AND src/mollie/mollie.test.ts::"getIntent maps a non-card method to wallet" AND src/mollie/mollie.test.ts::"getIntent yields undefined paymentMethod when method is missing"
Requirement: mapMollieStatus is exported and total
The package SHALL export mapMollieStatus(status: string): PaymentStatus.
The function SHALL map open → created, pending → pending,
authorized → authorized, paid → captured, canceled →
canceled, expired → canceled, failed → failed, and every
other string → failed.
Scenario: every Mollie status maps to the documented PaymentStatus
- WHEN
mapMollieStatusis called with each ofopen,pending,authorized,paid,canceled,expired,failed,unknown - THEN the results equal
created,pending,authorized,captured,canceled,canceled,failed,failedrespectively
Verified by: src/mollie/mollie.test.ts::"mapMollieStatus covers every defined Mollie status" AND src/mollie/mollie.test.ts::"mapMollieStatus falls back to failed on unknown status"
Requirement: mapMollieRefundStatus is exported and total
The package SHALL export
mapMollieRefundStatus(status: string): RefundStatus. The function
SHALL map refunded → succeeded, pending / queued / processing
→ pending, and every other string → failed.
Scenario: every Mollie refund status maps to the documented RefundStatus
- WHEN
mapMollieRefundStatusis called with each ofrefunded,pending,queued,processing,failed,unknown - THEN the results equal
succeeded,pending,pending,pending,failed,failedrespectively
Verified by: src/mollie/mollie.test.ts::"mapMollieRefundStatus covers every defined refund status" AND src/mollie/mollie.test.ts::"mapMollieRefundStatus falls back to failed on unknown status"
Requirement: formatMollieAmount and parseMollieAmount round-trip via bigint
The package SHALL export formatMollieAmount(amount: bigint): string
and parseMollieAmount(value: string): bigint. formatMollieAmount
SHALL produce a two-decimal string. parseMollieAmount SHALL accept
strings with no decimal, a single decimal place, two decimal places,
or more than two decimal places (truncating excess minor digits without
rounding). For every bigint n >= 0n,
parseMollieAmount(formatMollieAmount(n)) === n.
Scenario: one-cent amount formats with leading zero
- WHEN
formatMollieAmount(1n)is called - THEN the result is the string
"0.01"
Scenario: exact major-unit amount formats with trailing zeros
- WHEN
formatMollieAmount(100n)is called - THEN the result is
"1.00"
Scenario: large amount round-trips losslessly
- WHEN
formatMollieAmount(12345678901234n)is then passed back throughparseMollieAmount - THEN the parsed value equals
12345678901234n
Scenario: parse accepts short or missing minor digits
- WHEN
parseMollieAmount("10"),parseMollieAmount("10.5"), andparseMollieAmount("10.50")are called - THEN the results equal
1000n,1050n,1050nrespectively
Scenario: parse truncates over-precision
- WHEN
parseMollieAmount("10.123")is called - THEN the result is
1012n
Verified by: src/mollie/mollie.test.ts::"formatMollieAmount pads single cent to 0.01" AND src/mollie/mollie.test.ts::"formatMollieAmount produces two trailing zeros for exact major unit" AND src/mollie/mollie.test.ts::"formatMollieAmount handles large bigint values without loss" AND src/mollie/mollie.test.ts::"parseMollieAmount accepts no-decimal, single-decimal, and two-decimal strings" AND src/mollie/mollie.test.ts::"parseMollieAmount truncates over-precision without rounding" AND src/mollie/mollie.test.ts::"formatMollieAmount and parseMollieAmount round-trip for every minted bigint"
Requirement: failures raise PaymentProviderError only
The package SHALL NOT raise raw Error. Every failure path SHALL
surface a PaymentProviderError from @suaveplan/payment-providers
carrying a PAYMENT_PROVIDER_* code. Network failures (rejecting
fetch) SHALL map to PAYMENT_PROVIDER.HTTP_ERROR; non-2xx HTTP
responses SHALL also map to PAYMENT_PROVIDER.HTTP_ERROR.
Scenario: rejecting fetch surfaces HTTP_ERROR
- WHEN the injected
fetchrejects withnew TypeError("network down")duringgetIntent - THEN the call rejects with a
PaymentProviderErrorwhosecode === "PAYMENT_PROVIDER.HTTP_ERROR"and whoseprovider === "mollie"
Scenario: non-2xx response surfaces HTTP_ERROR
- WHEN Mollie responds with HTTP 422 during
createIntent - THEN the call rejects with
PaymentProviderError(PAYMENT_PROVIDER.HTTP_ERROR)andhttpStatus === 422
Verified by: src/mollie/mollie.test.ts::"surfaces a PaymentProviderError when fetch rejects" AND src/mollie/mollie.test.ts::"surfaces HTTP_ERROR when Mollie responds with 422"
Requirement: subpath exports and tier purity
The package SHALL ship subpath exports for the package root (.) and
the feature module (./mollie/mollie) in package.json#exports,
matching the binance sibling layout. The package SHALL be universal-tier
— no node:* imports, no browser-only globals — and SHALL list all
runtime deps (@suaveplan/payment, @suaveplan/payment-providers,
@suaveplan/error, tslib) in vite.config.ts#rollupOptions.external.
Scenario: subpath exports count
- WHEN
package.json#exportsis inspected - THEN the entries are exactly
./package.json,., and./mollie/mollie
Scenario: no Node-specific imports
- WHEN
src/is scanned fornode:import specifiers - THEN no matches are reported
Verified by: package.json#exports AND src/mollie/mollie.ts (file content)