@strav/payment 1.0.0-alpha.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +34 -0
- package/src/drivers/index.ts +6 -0
- package/src/drivers/mock_driver.ts +534 -0
- package/src/drivers/omise/index.ts +56 -0
- package/src/drivers/omise/omise_config.ts +19 -0
- package/src/drivers/omise/omise_driver.ts +576 -0
- package/src/drivers/omise/omise_mappers.ts +180 -0
- package/src/drivers/omise/omise_method_spec.ts +88 -0
- package/src/drivers/omise/omise_next_action_mapper.ts +89 -0
- package/src/drivers/omise/omise_price_spec.ts +85 -0
- package/src/drivers/omise/omise_provider.ts +33 -0
- package/src/drivers/omise/omise_schedule_mapper.ts +156 -0
- package/src/drivers/omise/omise_webhook.ts +162 -0
- package/src/drivers/payment_method_helpers.ts +35 -0
- package/src/drivers/stripe/index.ts +40 -0
- package/src/drivers/stripe/mappers/stripe_mappers.ts +312 -0
- package/src/drivers/stripe/mappers/stripe_method_spec.ts +77 -0
- package/src/drivers/stripe/mappers/stripe_next_action_mapper.ts +163 -0
- package/src/drivers/stripe/stripe_config.ts +18 -0
- package/src/drivers/stripe/stripe_driver.ts +650 -0
- package/src/drivers/stripe/stripe_provider.ts +38 -0
- package/src/drivers/stripe/webhook/stripe_normalize.ts +139 -0
- package/src/drivers/unsupported.ts +20 -0
- package/src/dto/index.ts +72 -0
- package/src/dto/payment_charge.ts +158 -0
- package/src/dto/payment_checkout.ts +46 -0
- package/src/dto/payment_customer.ts +52 -0
- package/src/dto/payment_event.ts +83 -0
- package/src/dto/payment_invoice.ts +39 -0
- package/src/dto/payment_link.ts +81 -0
- package/src/dto/payment_method.ts +43 -0
- package/src/dto/payment_price.ts +47 -0
- package/src/dto/payment_product.ts +40 -0
- package/src/dto/payment_subscription.ts +71 -0
- package/src/index.ts +78 -0
- package/src/ledger/apply_payment_ledger_migration.ts +106 -0
- package/src/ledger/index.ts +13 -0
- package/src/ledger/payment_ledger.ts +260 -0
- package/src/ledger/payment_ledger_models.ts +66 -0
- package/src/ledger/schemas/payment_customer_schema.ts +34 -0
- package/src/ledger/schemas/payment_invoice_schema.ts +39 -0
- package/src/ledger/schemas/payment_subscription_schema.ts +34 -0
- package/src/payment_capabilities.ts +91 -0
- package/src/payment_driver.ts +167 -0
- package/src/payment_error.ts +159 -0
- package/src/payment_manager.ts +174 -0
- package/src/payment_provider.ts +93 -0
- package/src/tenant_metadata.ts +60 -0
- package/src/types.ts +49 -0
- package/src/webhook/index.ts +8 -0
- package/src/webhook/payment_webhook.ts +190 -0
- package/src/webhook/payment_webhook_event.ts +22 -0
- package/src/webhook/payment_webhook_event_repository.ts +65 -0
- package/src/webhook/payment_webhook_event_schema.ts +40 -0
- package/src/webhook/payment_webhook_registry.ts +65 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `stripeNormalize(event)` — map a `Stripe.Event` onto the
|
|
3
|
+
* framework's `NormalizedWebhookEvent` (or `null` for events
|
|
4
|
+
* outside the closed union).
|
|
5
|
+
*
|
|
6
|
+
* The mapping table covers the events apps most often care
|
|
7
|
+
* about. Extending it is additive — adding a new case never
|
|
8
|
+
* breaks an existing handler, and unmapped events still flow
|
|
9
|
+
* through the dedup ledger (with no user dispatch).
|
|
10
|
+
*
|
|
11
|
+
* `_fields` carries the parsed shape the `PaymentLedger`
|
|
12
|
+
* consumes when ledger sync is on. Keys mirror the matching
|
|
13
|
+
* DTO field names so the ledger can write directly.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type Stripe from 'stripe'
|
|
17
|
+
import type {
|
|
18
|
+
NormalizedWebhookEvent,
|
|
19
|
+
PaymentEventType,
|
|
20
|
+
} from '../../../dto/index.ts'
|
|
21
|
+
import { readTenantId } from '../../../tenant_metadata.ts'
|
|
22
|
+
import {
|
|
23
|
+
toPaymentCustomer,
|
|
24
|
+
toPaymentInvoice,
|
|
25
|
+
toPaymentSubscription,
|
|
26
|
+
} from '../mappers/stripe_mappers.ts'
|
|
27
|
+
|
|
28
|
+
const TYPE_MAP: Record<string, PaymentEventType> = {
|
|
29
|
+
'customer.created': 'customer.created',
|
|
30
|
+
'customer.updated': 'customer.updated',
|
|
31
|
+
'customer.deleted': 'customer.deleted',
|
|
32
|
+
'customer.subscription.created': 'subscription.created',
|
|
33
|
+
'customer.subscription.updated': 'subscription.updated',
|
|
34
|
+
'customer.subscription.deleted': 'subscription.canceled',
|
|
35
|
+
'customer.subscription.trial_will_end': 'subscription.trial_will_end',
|
|
36
|
+
'charge.succeeded': 'charge.succeeded',
|
|
37
|
+
'charge.failed': 'charge.failed',
|
|
38
|
+
'charge.refunded': 'charge.refunded',
|
|
39
|
+
'invoice.created': 'invoice.created',
|
|
40
|
+
'invoice.paid': 'invoice.paid',
|
|
41
|
+
'invoice.payment_failed': 'invoice.payment_failed',
|
|
42
|
+
'invoice.voided': 'invoice.voided',
|
|
43
|
+
'checkout.session.completed': 'checkout.completed',
|
|
44
|
+
'checkout.session.expired': 'checkout.expired',
|
|
45
|
+
'payment_method.attached': 'payment_method.attached',
|
|
46
|
+
'payment_method.detached': 'payment_method.detached',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function stripeNormalize(event: Stripe.Event): NormalizedWebhookEvent | null {
|
|
50
|
+
const type = TYPE_MAP[event.type]
|
|
51
|
+
if (!type) return null
|
|
52
|
+
|
|
53
|
+
const data: NormalizedWebhookEvent['data'] = {}
|
|
54
|
+
let fields: Record<string, unknown> | undefined
|
|
55
|
+
|
|
56
|
+
switch (type) {
|
|
57
|
+
case 'customer.created':
|
|
58
|
+
case 'customer.updated': {
|
|
59
|
+
const c = event.data.object as Stripe.Customer
|
|
60
|
+
data.customerId = c.id
|
|
61
|
+
const dto = toPaymentCustomer(c)
|
|
62
|
+
fields = { ...dto }
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
case 'customer.deleted': {
|
|
66
|
+
const c = event.data.object as Stripe.Customer
|
|
67
|
+
data.customerId = c.id
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
case 'subscription.created':
|
|
71
|
+
case 'subscription.updated':
|
|
72
|
+
case 'subscription.canceled':
|
|
73
|
+
case 'subscription.trial_will_end': {
|
|
74
|
+
const s = event.data.object as Stripe.Subscription
|
|
75
|
+
const dto = toPaymentSubscription(s)
|
|
76
|
+
data.subscriptionId = dto.id
|
|
77
|
+
data.customerId = dto.customerId
|
|
78
|
+
fields = { ...dto }
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
case 'charge.succeeded':
|
|
82
|
+
case 'charge.failed':
|
|
83
|
+
case 'charge.refunded': {
|
|
84
|
+
const c = event.data.object as Stripe.Charge
|
|
85
|
+
data.chargeId = c.id
|
|
86
|
+
if (typeof c.customer === 'string') data.customerId = c.customer
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
case 'invoice.created':
|
|
90
|
+
case 'invoice.paid':
|
|
91
|
+
case 'invoice.payment_failed':
|
|
92
|
+
case 'invoice.voided': {
|
|
93
|
+
const i = event.data.object as Stripe.Invoice
|
|
94
|
+
const dto = toPaymentInvoice(i)
|
|
95
|
+
data.invoiceId = dto.id
|
|
96
|
+
data.customerId = dto.customerId
|
|
97
|
+
if (dto.subscriptionId) data.subscriptionId = dto.subscriptionId
|
|
98
|
+
fields = { ...dto }
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
case 'checkout.completed':
|
|
102
|
+
case 'checkout.expired': {
|
|
103
|
+
const s = event.data.object as Stripe.Checkout.Session
|
|
104
|
+
data.checkoutId = s.id
|
|
105
|
+
if (typeof s.customer === 'string') data.customerId = s.customer
|
|
106
|
+
if (typeof s.subscription === 'string') data.subscriptionId = s.subscription
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
case 'payment_method.attached':
|
|
110
|
+
case 'payment_method.detached': {
|
|
111
|
+
const pm = event.data.object as Stripe.PaymentMethod
|
|
112
|
+
data.paymentMethodId = pm.id
|
|
113
|
+
if (typeof pm.customer === 'string') data.customerId = pm.customer
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Read the framework's tenant key off whatever resource the
|
|
119
|
+
// event carries. Stripe echoes `metadata` on every resource
|
|
120
|
+
// type, so the same lookup works across customer / subscription
|
|
121
|
+
// / invoice / charge / checkout payloads.
|
|
122
|
+
const resourceMeta =
|
|
123
|
+
(event.data.object as { metadata?: Record<string, unknown> } | undefined)
|
|
124
|
+
?.metadata ?? null
|
|
125
|
+
const tenantId = readTenantId(resourceMeta)
|
|
126
|
+
|
|
127
|
+
const normalized: NormalizedWebhookEvent = {
|
|
128
|
+
id: event.id,
|
|
129
|
+
type,
|
|
130
|
+
provider: 'stripe',
|
|
131
|
+
raw: event,
|
|
132
|
+
data,
|
|
133
|
+
...(tenantId ? { tenantId } : {}),
|
|
134
|
+
}
|
|
135
|
+
if (fields) {
|
|
136
|
+
;(normalized as { _fields?: unknown })._fields = fields
|
|
137
|
+
}
|
|
138
|
+
return normalized
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `unsupported(provider, operation)` — helper for drivers to
|
|
3
|
+
* stub out *Ops methods they can't fulfil. Returns a function
|
|
4
|
+
* that throws `ProviderUnsupportedError` synchronously when
|
|
5
|
+
* called. Drivers attach the returned function to the relevant
|
|
6
|
+
* `*Ops` key and omit the matching `PaymentCapability` from
|
|
7
|
+
* `capabilities`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ProviderUnsupportedError } from '../payment_error.ts'
|
|
11
|
+
|
|
12
|
+
export function unsupported(
|
|
13
|
+
provider: string,
|
|
14
|
+
operation: string,
|
|
15
|
+
reason?: string,
|
|
16
|
+
): (...args: unknown[]) => never {
|
|
17
|
+
return () => {
|
|
18
|
+
throw new ProviderUnsupportedError(provider, operation, reason ? { reason } : {})
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/dto/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Barrel for normalized DTOs + input shapes. */
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
CreateCustomerInput,
|
|
5
|
+
ListCustomersOptions,
|
|
6
|
+
PaginatedCustomers,
|
|
7
|
+
PaymentCustomer,
|
|
8
|
+
UpdateCustomerInput,
|
|
9
|
+
} from './payment_customer.ts'
|
|
10
|
+
export type {
|
|
11
|
+
CreateProductInput,
|
|
12
|
+
ListProductsOptions,
|
|
13
|
+
PaginatedProducts,
|
|
14
|
+
PaymentProduct,
|
|
15
|
+
UpdateProductInput,
|
|
16
|
+
} from './payment_product.ts'
|
|
17
|
+
export type {
|
|
18
|
+
CreatePriceInput,
|
|
19
|
+
ListPricesOptions,
|
|
20
|
+
PaginatedPrices,
|
|
21
|
+
PaymentPrice,
|
|
22
|
+
} from './payment_price.ts'
|
|
23
|
+
export type {
|
|
24
|
+
CancelSubscriptionOptions,
|
|
25
|
+
CreateSubscriptionInput,
|
|
26
|
+
ListSubscriptionsOptions,
|
|
27
|
+
PaginatedSubscriptions,
|
|
28
|
+
PaymentSubscription,
|
|
29
|
+
SubscriptionStatus,
|
|
30
|
+
UpdateSubscriptionInput,
|
|
31
|
+
} from './payment_subscription.ts'
|
|
32
|
+
export type {
|
|
33
|
+
ListPaymentMethodsOptions,
|
|
34
|
+
PaginatedPaymentMethods,
|
|
35
|
+
PaymentMethod,
|
|
36
|
+
PaymentMethodKind,
|
|
37
|
+
} from './payment_method.ts'
|
|
38
|
+
export type {
|
|
39
|
+
ChargeStatus,
|
|
40
|
+
CreateChargeInput,
|
|
41
|
+
CreateRefundInput,
|
|
42
|
+
PaymentCharge,
|
|
43
|
+
PaymentMethodSpec,
|
|
44
|
+
PaymentNextAction,
|
|
45
|
+
PaymentRefund,
|
|
46
|
+
} from './payment_charge.ts'
|
|
47
|
+
export type {
|
|
48
|
+
InvoiceStatus,
|
|
49
|
+
ListInvoicesOptions,
|
|
50
|
+
PaginatedInvoices,
|
|
51
|
+
PaymentInvoice,
|
|
52
|
+
} from './payment_invoice.ts'
|
|
53
|
+
export type {
|
|
54
|
+
CheckoutLineItem,
|
|
55
|
+
CheckoutMode,
|
|
56
|
+
CheckoutStatus,
|
|
57
|
+
CreateCheckoutInput,
|
|
58
|
+
PaymentCheckoutSession,
|
|
59
|
+
} from './payment_checkout.ts'
|
|
60
|
+
export type {
|
|
61
|
+
CreatePaymentLinkInput,
|
|
62
|
+
ListPaymentLinksOptions,
|
|
63
|
+
PaginatedPaymentLinks,
|
|
64
|
+
PaymentLink,
|
|
65
|
+
} from './payment_link.ts'
|
|
66
|
+
export type {
|
|
67
|
+
NormalizedWebhookEvent,
|
|
68
|
+
PaymentEventType,
|
|
69
|
+
WebhookHandler,
|
|
70
|
+
WebhookHandlerContext,
|
|
71
|
+
WebhookHandlerFilter,
|
|
72
|
+
} from './payment_event.ts'
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PaymentCharge` — normalized one-shot charge result.
|
|
3
|
+
*
|
|
4
|
+
* Both successful and failed attempts are returned via this DTO
|
|
5
|
+
* (use `status` to distinguish). Vendor exceptions for invalid
|
|
6
|
+
* input — bad currency code, missing customer — are still raised
|
|
7
|
+
* as `PaymentProviderError`.
|
|
8
|
+
*
|
|
9
|
+
* Async payment methods (PromptPay, PayNow, redirect wallets,
|
|
10
|
+
* 3DS bank challenges, convenience-store vouchers) settle in two
|
|
11
|
+
* steps: the framework returns `status: 'requires_action'` plus a
|
|
12
|
+
* `nextAction` discriminator the app uses to drive the UI (show
|
|
13
|
+
* QR, redirect to URL, render voucher). Settlement arrives via
|
|
14
|
+
* the provider's `charge.succeeded` / `charge.failed` webhook.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type ChargeStatus =
|
|
18
|
+
| 'succeeded'
|
|
19
|
+
| 'pending'
|
|
20
|
+
| 'failed'
|
|
21
|
+
| 'refunded'
|
|
22
|
+
| 'partial_refunded'
|
|
23
|
+
| 'requires_action'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What the app must do next to drive an async charge to
|
|
27
|
+
* settlement. Null on synchronous (card) charges.
|
|
28
|
+
*
|
|
29
|
+
* - `display_qr` show a QR the customer scans with their banking app
|
|
30
|
+
* (PromptPay, PayNow, FPS, WeChat Pay in some flows).
|
|
31
|
+
* `qrData` is the encoded string; `qrImageUrl` is a
|
|
32
|
+
* hosted PNG when the provider gives one.
|
|
33
|
+
* - `redirect` send the customer to a wallet / bank page
|
|
34
|
+
* (TrueMoney, Alipay, GrabPay, KakaoPay, …).
|
|
35
|
+
* Returns to `CreateChargeInput.returnUrl`.
|
|
36
|
+
* - `authorize` 3DS bank challenge — same shape as `redirect` but
|
|
37
|
+
* semantically distinct (card holder verification,
|
|
38
|
+
* not a wallet handoff).
|
|
39
|
+
* - `voucher` convenience-store voucher / Boleto — show the
|
|
40
|
+
* reference number + barcode for the customer to
|
|
41
|
+
* pay at a physical counter.
|
|
42
|
+
* - `wait` the charge is in flight on bank rails; nothing
|
|
43
|
+
* to display. Apps poll or wait for the webhook.
|
|
44
|
+
*/
|
|
45
|
+
export type PaymentNextAction =
|
|
46
|
+
| { kind: 'display_qr'; qrData: string; qrImageUrl?: string; expiresAt?: Date }
|
|
47
|
+
| { kind: 'redirect'; url: string; expiresAt?: Date }
|
|
48
|
+
| { kind: 'authorize'; url: string; expiresAt?: Date }
|
|
49
|
+
| { kind: 'voucher'; reference: string; barcodeImageUrl?: string; expiresAt?: Date }
|
|
50
|
+
| { kind: 'wait' }
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Structured payment-method spec — drivers materialize the
|
|
54
|
+
* underlying source / token / etc. server-side. Apps that have
|
|
55
|
+
* a pre-tokenized card id can keep passing a string for
|
|
56
|
+
* back-compatibility; the spec is required for QR / wallet /
|
|
57
|
+
* voucher methods because there's no single id to pass.
|
|
58
|
+
*
|
|
59
|
+
* Open-by-extension via the framework's `PaymentMethodSpec`
|
|
60
|
+
* union: when a future driver supports a method not listed here,
|
|
61
|
+
* the union grows and existing drivers throw
|
|
62
|
+
* `ProviderUnsupportedError` until they implement it.
|
|
63
|
+
*/
|
|
64
|
+
export type PaymentMethodSpec =
|
|
65
|
+
| { kind: 'card'; token: string }
|
|
66
|
+
| { kind: 'promptpay' }
|
|
67
|
+
| { kind: 'paynow' }
|
|
68
|
+
| { kind: 'fps' }
|
|
69
|
+
| { kind: 'truemoney'; phoneNumber: string }
|
|
70
|
+
| { kind: 'alipay' }
|
|
71
|
+
| { kind: 'wechat_pay' }
|
|
72
|
+
| { kind: 'grabpay' }
|
|
73
|
+
| { kind: 'kakaopay' }
|
|
74
|
+
| { kind: 'rabbit_linepay' }
|
|
75
|
+
| { kind: 'konbini'; phoneNumber?: string }
|
|
76
|
+
|
|
77
|
+
export interface PaymentCharge {
|
|
78
|
+
id: string
|
|
79
|
+
provider: string
|
|
80
|
+
customerId: string | null
|
|
81
|
+
amount: number
|
|
82
|
+
currency: string
|
|
83
|
+
status: ChargeStatus
|
|
84
|
+
paymentMethodId: string | null
|
|
85
|
+
/** Failure code from the provider — `'card_declined'`, etc. Stable across drivers when possible. */
|
|
86
|
+
failureCode: string | null
|
|
87
|
+
failureMessage: string | null
|
|
88
|
+
/**
|
|
89
|
+
* Populated when `status === 'requires_action'` (and sometimes
|
|
90
|
+
* `'pending'`). Null for synchronous charges that settled
|
|
91
|
+
* immediately.
|
|
92
|
+
*/
|
|
93
|
+
nextAction: PaymentNextAction | null
|
|
94
|
+
metadata: Record<string, string>
|
|
95
|
+
createdAt: Date
|
|
96
|
+
raw: unknown
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CreateChargeInput {
|
|
100
|
+
amount: number
|
|
101
|
+
currency: string
|
|
102
|
+
customer?: string
|
|
103
|
+
/**
|
|
104
|
+
* Either a pre-tokenized payment-method id (`'pm_xxx'` /
|
|
105
|
+
* `'tokn_xxx'`) — the v1 shape, kept for back-compat — or a
|
|
106
|
+
* structured spec (`{ kind: 'promptpay' }`, `{ kind: 'card',
|
|
107
|
+
* token: 'tokn_xxx' }`, …). Specs are required for methods
|
|
108
|
+
* that have no single id to reference (QR / wallet / voucher).
|
|
109
|
+
*/
|
|
110
|
+
paymentMethod?: string | PaymentMethodSpec
|
|
111
|
+
description?: string
|
|
112
|
+
metadata?: Record<string, string>
|
|
113
|
+
/**
|
|
114
|
+
* Stripe-style: `true` triggers immediate capture; `false`
|
|
115
|
+
* authorises only (apps call `capture` later). Drivers without
|
|
116
|
+
* holds throw `ProviderUnsupportedError` when `false`.
|
|
117
|
+
*/
|
|
118
|
+
capture?: boolean
|
|
119
|
+
/**
|
|
120
|
+
* Where the provider returns the customer after a `redirect`
|
|
121
|
+
* or `authorize` next-action. Required for redirect wallets +
|
|
122
|
+
* 3DS challenges; ignored for synchronous card charges.
|
|
123
|
+
* Apps usually set a global default via
|
|
124
|
+
* `config.payment.returnUrl` and override per-call when
|
|
125
|
+
* needed.
|
|
126
|
+
*/
|
|
127
|
+
returnUrl?: string
|
|
128
|
+
/**
|
|
129
|
+
* Provider-side idempotency key. Drivers with the `idempotency`
|
|
130
|
+
* capability dedup retried calls with the same key for ~24h
|
|
131
|
+
* (Stripe). Drivers without the capability silently ignore
|
|
132
|
+
* — apps that need guaranteed dedup on those providers build
|
|
133
|
+
* it app-side (claim the key in a DB table before calling).
|
|
134
|
+
*/
|
|
135
|
+
idempotencyKey?: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface CreateRefundInput {
|
|
139
|
+
charge: string
|
|
140
|
+
/** Amount in minor unit. Omitted = full refund. */
|
|
141
|
+
amount?: number
|
|
142
|
+
reason?: string
|
|
143
|
+
metadata?: Record<string, string>
|
|
144
|
+
/** See `CreateChargeInput.idempotencyKey`. */
|
|
145
|
+
idempotencyKey?: string
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface PaymentRefund {
|
|
149
|
+
id: string
|
|
150
|
+
provider: string
|
|
151
|
+
chargeId: string
|
|
152
|
+
amount: number
|
|
153
|
+
currency: string
|
|
154
|
+
status: 'pending' | 'succeeded' | 'failed'
|
|
155
|
+
reason: string | null
|
|
156
|
+
createdAt: Date
|
|
157
|
+
raw: unknown
|
|
158
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PaymentCheckoutSession` — normalized hosted-checkout session.
|
|
3
|
+
* Apps redirect the customer to `url`; the provider walks them
|
|
4
|
+
* through payment and posts back a `checkout.completed` webhook
|
|
5
|
+
* when done.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type CheckoutMode = 'payment' | 'subscription' | 'setup'
|
|
9
|
+
|
|
10
|
+
export type CheckoutStatus = 'open' | 'complete' | 'expired'
|
|
11
|
+
|
|
12
|
+
export interface CheckoutLineItem {
|
|
13
|
+
price: string
|
|
14
|
+
quantity?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PaymentCheckoutSession {
|
|
18
|
+
id: string
|
|
19
|
+
provider: string
|
|
20
|
+
mode: CheckoutMode
|
|
21
|
+
status: CheckoutStatus
|
|
22
|
+
/** Hosted-checkout URL. Apps redirect the user here. */
|
|
23
|
+
url: string
|
|
24
|
+
customerId: string | null
|
|
25
|
+
/** Once `complete`, the resulting subscription / payment id (driver-specific shape). */
|
|
26
|
+
paymentIntentId: string | null
|
|
27
|
+
subscriptionId: string | null
|
|
28
|
+
expiresAt: Date | null
|
|
29
|
+
metadata: Record<string, string>
|
|
30
|
+
createdAt: Date
|
|
31
|
+
raw: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CreateCheckoutInput {
|
|
35
|
+
mode: CheckoutMode
|
|
36
|
+
items: readonly CheckoutLineItem[]
|
|
37
|
+
successUrl: string
|
|
38
|
+
cancelUrl: string
|
|
39
|
+
customer?: string
|
|
40
|
+
customerEmail?: string
|
|
41
|
+
/** Trial days when `mode === 'subscription'`. */
|
|
42
|
+
trialDays?: number
|
|
43
|
+
metadata?: Record<string, string>
|
|
44
|
+
/** See `CreateChargeInput.idempotencyKey`. */
|
|
45
|
+
idempotencyKey?: string
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PaymentCustomer` — normalized view of a customer record
|
|
3
|
+
* across providers. Provider-specific shape lives in `.raw`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PaymentCustomer {
|
|
7
|
+
/** Provider-native id (`cus_xxx` for Stripe, `ctm_xxx` for Stripe, etc.). */
|
|
8
|
+
id: string
|
|
9
|
+
/** Driver name — `'stripe'`, `'paddle'`, `'omise'`, … */
|
|
10
|
+
provider: string
|
|
11
|
+
email: string
|
|
12
|
+
name?: string
|
|
13
|
+
phone?: string
|
|
14
|
+
metadata: Record<string, string>
|
|
15
|
+
createdAt: Date
|
|
16
|
+
/** Native provider object. Use only when you need a field the normalized DTO doesn't expose. */
|
|
17
|
+
raw: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CreateCustomerInput {
|
|
21
|
+
email: string
|
|
22
|
+
name?: string
|
|
23
|
+
phone?: string
|
|
24
|
+
metadata?: Record<string, string>
|
|
25
|
+
/**
|
|
26
|
+
* Provider-side idempotency key. Drivers with the `idempotency`
|
|
27
|
+
* capability dedup retried calls with the same key for ~24h
|
|
28
|
+
* (Stripe). Drivers without the capability silently ignore.
|
|
29
|
+
*/
|
|
30
|
+
idempotencyKey?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UpdateCustomerInput {
|
|
34
|
+
email?: string
|
|
35
|
+
name?: string
|
|
36
|
+
phone?: string
|
|
37
|
+
metadata?: Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ListCustomersOptions {
|
|
41
|
+
/** Driver-defined cursor — passed through verbatim to the next page call. */
|
|
42
|
+
cursor?: string
|
|
43
|
+
limit?: number
|
|
44
|
+
/** Email filter — exact match. Drivers that don't support filtering ignore this and apps filter client-side. */
|
|
45
|
+
email?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PaginatedCustomers {
|
|
49
|
+
data: PaymentCustomer[]
|
|
50
|
+
/** Opaque cursor for the next page. `null` when the listing is exhausted. */
|
|
51
|
+
nextCursor: string | null
|
|
52
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized webhook event types. Drivers map their native event
|
|
3
|
+
* shapes (`invoice.paid`, `subscription_created`, …) onto this
|
|
4
|
+
* closed union. Apps register handlers by normalized type; the
|
|
5
|
+
* native event is on `ctx.raw`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type PaymentEventType =
|
|
9
|
+
| 'customer.created'
|
|
10
|
+
| 'customer.updated'
|
|
11
|
+
| 'customer.deleted'
|
|
12
|
+
| 'subscription.created'
|
|
13
|
+
| 'subscription.updated'
|
|
14
|
+
| 'subscription.canceled'
|
|
15
|
+
| 'subscription.trial_will_end'
|
|
16
|
+
| 'charge.succeeded'
|
|
17
|
+
| 'charge.failed'
|
|
18
|
+
| 'charge.refunded'
|
|
19
|
+
| 'invoice.created'
|
|
20
|
+
| 'invoice.paid'
|
|
21
|
+
| 'invoice.payment_failed'
|
|
22
|
+
| 'invoice.voided'
|
|
23
|
+
| 'checkout.completed'
|
|
24
|
+
| 'checkout.expired'
|
|
25
|
+
| 'payment_method.attached'
|
|
26
|
+
| 'payment_method.detached'
|
|
27
|
+
|
|
28
|
+
export interface NormalizedWebhookEvent {
|
|
29
|
+
/** Driver-assigned id; the dedup key. */
|
|
30
|
+
id: string
|
|
31
|
+
/** Normalized type from the closed union above. */
|
|
32
|
+
type: PaymentEventType
|
|
33
|
+
/**
|
|
34
|
+
* After the dispatcher routes the event, this is the app-chosen
|
|
35
|
+
* **instance name** (the `:provider` route param, matches
|
|
36
|
+
* `payment.use(name)`). Drivers' `normalize` set it to the
|
|
37
|
+
* driver name (`'stripe'` / `'omise'`); the dispatcher
|
|
38
|
+
* overrides with the instance name.
|
|
39
|
+
*/
|
|
40
|
+
provider: string
|
|
41
|
+
/**
|
|
42
|
+
* Strav tenant id pulled from the original create call's
|
|
43
|
+
* metadata (`metadata.strav_tenant_id`). When set, the
|
|
44
|
+
* dispatcher wraps ledger writes + user handlers in
|
|
45
|
+
* `TenantManager.withTenant(tenantId, ...)`. Undefined when
|
|
46
|
+
* the originating call didn't stamp a tenant — multi-tenant
|
|
47
|
+
* apps that forget the stamp see ledger writes skipped + a
|
|
48
|
+
* one-shot warning.
|
|
49
|
+
*/
|
|
50
|
+
tenantId?: string
|
|
51
|
+
/** Native provider event payload. */
|
|
52
|
+
raw: unknown
|
|
53
|
+
/** Convenience accessors for the most common downstream resources. */
|
|
54
|
+
data: {
|
|
55
|
+
customerId?: string
|
|
56
|
+
subscriptionId?: string
|
|
57
|
+
invoiceId?: string
|
|
58
|
+
chargeId?: string
|
|
59
|
+
checkoutId?: string
|
|
60
|
+
paymentMethodId?: string
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface WebhookHandlerContext {
|
|
65
|
+
event: NormalizedWebhookEvent
|
|
66
|
+
/** Convenience shortcut for `event.id`. */
|
|
67
|
+
eventId: string
|
|
68
|
+
/** Convenience shortcut for `event.type`. */
|
|
69
|
+
eventType: PaymentEventType
|
|
70
|
+
/** Convenience shortcut for `event.provider`. */
|
|
71
|
+
provider: string
|
|
72
|
+
/** Convenience shortcut for `event.tenantId`. */
|
|
73
|
+
tenantId?: string
|
|
74
|
+
/** Convenience shortcut for `event.raw`. */
|
|
75
|
+
raw: unknown
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type WebhookHandler = (ctx: WebhookHandlerContext) => void | Promise<void>
|
|
79
|
+
|
|
80
|
+
export interface WebhookHandlerFilter {
|
|
81
|
+
/** Restrict the handler to one provider. Omitted = fires for any provider that emits the type. */
|
|
82
|
+
provider?: string
|
|
83
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PaymentInvoice` — normalized invoice record (whether issued
|
|
3
|
+
* by a subscription cycle or manually drafted).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type InvoiceStatus = 'draft' | 'open' | 'paid' | 'uncollectible' | 'void'
|
|
7
|
+
|
|
8
|
+
export interface PaymentInvoice {
|
|
9
|
+
id: string
|
|
10
|
+
provider: string
|
|
11
|
+
customerId: string
|
|
12
|
+
subscriptionId: string | null
|
|
13
|
+
status: InvoiceStatus
|
|
14
|
+
amount: number
|
|
15
|
+
amountPaid: number
|
|
16
|
+
amountDue: number
|
|
17
|
+
currency: string
|
|
18
|
+
/** Provider-hosted invoice URL — null when not hosted (e.g., draft state). */
|
|
19
|
+
hostedUrl: string | null
|
|
20
|
+
pdfUrl: string | null
|
|
21
|
+
dueAt: Date | null
|
|
22
|
+
paidAt: Date | null
|
|
23
|
+
metadata: Record<string, string>
|
|
24
|
+
createdAt: Date
|
|
25
|
+
raw: unknown
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ListInvoicesOptions {
|
|
29
|
+
customer?: string
|
|
30
|
+
subscription?: string
|
|
31
|
+
status?: InvoiceStatus
|
|
32
|
+
cursor?: string
|
|
33
|
+
limit?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PaginatedInvoices {
|
|
37
|
+
data: PaymentInvoice[]
|
|
38
|
+
nextCursor: string | null
|
|
39
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PaymentLink` — normalized shareable payment URL.
|
|
3
|
+
*
|
|
4
|
+
* A payment link is a one-shot hosted page the customer opens to
|
|
5
|
+
* pay. Distinct from `PaymentCheckoutSession` because links are
|
|
6
|
+
* meant to be shared (email, SMS, QR code on a poster) and reused
|
|
7
|
+
* across customers; checkout sessions are tied to a single
|
|
8
|
+
* customer journey.
|
|
9
|
+
*
|
|
10
|
+
* Provider divergence the framework intentionally surfaces:
|
|
11
|
+
*
|
|
12
|
+
* - **Stripe** requires a catalogue Price id (`items` input);
|
|
13
|
+
* ad-hoc amounts aren't supported. Apps create a Price first
|
|
14
|
+
* via `payment.prices.create({...})` then pass `items`.
|
|
15
|
+
* - **Omise** has no Prices catalogue. Apps pass `amount`,
|
|
16
|
+
* `currency`, `title`, `description` directly.
|
|
17
|
+
*
|
|
18
|
+
* Both inputs are optional on `CreatePaymentLinkInput`; drivers
|
|
19
|
+
* validate which shape they need and throw a clear
|
|
20
|
+
* `PaymentConfigError` when the wrong one is passed.
|
|
21
|
+
*
|
|
22
|
+
* Lifecycle:
|
|
23
|
+
*
|
|
24
|
+
* - `active === true` — link accepts new payments.
|
|
25
|
+
* - `active === false` — link is deactivated; existing
|
|
26
|
+
* in-flight payments still settle.
|
|
27
|
+
* - `reusable === false` — single-use; the link becomes
|
|
28
|
+
* `used` after the first successful payment (Omise's default).
|
|
29
|
+
* - `reusable === true` — repeatable; Stripe's default.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export interface PaymentLink {
|
|
33
|
+
id: string
|
|
34
|
+
provider: string
|
|
35
|
+
/** Hosted-checkout URL the customer opens to pay. */
|
|
36
|
+
url: string
|
|
37
|
+
/** Ad-hoc amount in minor units. `null` when Stripe link references a Price. */
|
|
38
|
+
amount: number | null
|
|
39
|
+
/** ISO 4217 (lowercase). `null` when Stripe link is multi-price. */
|
|
40
|
+
currency: string | null
|
|
41
|
+
active: boolean
|
|
42
|
+
/** True when the link can take multiple payments. */
|
|
43
|
+
reusable: boolean
|
|
44
|
+
title?: string
|
|
45
|
+
description?: string
|
|
46
|
+
metadata: Record<string, string>
|
|
47
|
+
createdAt: Date
|
|
48
|
+
raw: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CreatePaymentLinkInput {
|
|
52
|
+
/** Stripe path: required. Omise: throw ProviderUnsupportedError if passed. */
|
|
53
|
+
items?: ReadonlyArray<{ price: string; quantity?: number }>
|
|
54
|
+
/** Omise path: amount + currency + title + description. Stripe: throw if used without `items`. */
|
|
55
|
+
amount?: number
|
|
56
|
+
currency?: string
|
|
57
|
+
title?: string
|
|
58
|
+
description?: string
|
|
59
|
+
/**
|
|
60
|
+
* Whether the link can take multiple payments. Stripe defaults
|
|
61
|
+
* to `true`; Omise to `false`. Apps that need uniform behaviour
|
|
62
|
+
* pass an explicit value.
|
|
63
|
+
*/
|
|
64
|
+
reusable?: boolean
|
|
65
|
+
metadata?: Record<string, string>
|
|
66
|
+
/** Stripe: redirect URL after completion. Ignored by Omise (uses provider-default success page). */
|
|
67
|
+
afterCompletionRedirect?: string
|
|
68
|
+
/** See `CreateChargeInput.idempotencyKey`. */
|
|
69
|
+
idempotencyKey?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ListPaymentLinksOptions {
|
|
73
|
+
cursor?: string
|
|
74
|
+
limit?: number
|
|
75
|
+
active?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PaginatedPaymentLinks {
|
|
79
|
+
data: PaymentLink[]
|
|
80
|
+
nextCursor: string | null
|
|
81
|
+
}
|