@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.
Files changed (55) hide show
  1. package/package.json +34 -0
  2. package/src/drivers/index.ts +6 -0
  3. package/src/drivers/mock_driver.ts +534 -0
  4. package/src/drivers/omise/index.ts +56 -0
  5. package/src/drivers/omise/omise_config.ts +19 -0
  6. package/src/drivers/omise/omise_driver.ts +576 -0
  7. package/src/drivers/omise/omise_mappers.ts +180 -0
  8. package/src/drivers/omise/omise_method_spec.ts +88 -0
  9. package/src/drivers/omise/omise_next_action_mapper.ts +89 -0
  10. package/src/drivers/omise/omise_price_spec.ts +85 -0
  11. package/src/drivers/omise/omise_provider.ts +33 -0
  12. package/src/drivers/omise/omise_schedule_mapper.ts +156 -0
  13. package/src/drivers/omise/omise_webhook.ts +162 -0
  14. package/src/drivers/payment_method_helpers.ts +35 -0
  15. package/src/drivers/stripe/index.ts +40 -0
  16. package/src/drivers/stripe/mappers/stripe_mappers.ts +312 -0
  17. package/src/drivers/stripe/mappers/stripe_method_spec.ts +77 -0
  18. package/src/drivers/stripe/mappers/stripe_next_action_mapper.ts +163 -0
  19. package/src/drivers/stripe/stripe_config.ts +18 -0
  20. package/src/drivers/stripe/stripe_driver.ts +650 -0
  21. package/src/drivers/stripe/stripe_provider.ts +38 -0
  22. package/src/drivers/stripe/webhook/stripe_normalize.ts +139 -0
  23. package/src/drivers/unsupported.ts +20 -0
  24. package/src/dto/index.ts +72 -0
  25. package/src/dto/payment_charge.ts +158 -0
  26. package/src/dto/payment_checkout.ts +46 -0
  27. package/src/dto/payment_customer.ts +52 -0
  28. package/src/dto/payment_event.ts +83 -0
  29. package/src/dto/payment_invoice.ts +39 -0
  30. package/src/dto/payment_link.ts +81 -0
  31. package/src/dto/payment_method.ts +43 -0
  32. package/src/dto/payment_price.ts +47 -0
  33. package/src/dto/payment_product.ts +40 -0
  34. package/src/dto/payment_subscription.ts +71 -0
  35. package/src/index.ts +78 -0
  36. package/src/ledger/apply_payment_ledger_migration.ts +106 -0
  37. package/src/ledger/index.ts +13 -0
  38. package/src/ledger/payment_ledger.ts +260 -0
  39. package/src/ledger/payment_ledger_models.ts +66 -0
  40. package/src/ledger/schemas/payment_customer_schema.ts +34 -0
  41. package/src/ledger/schemas/payment_invoice_schema.ts +39 -0
  42. package/src/ledger/schemas/payment_subscription_schema.ts +34 -0
  43. package/src/payment_capabilities.ts +91 -0
  44. package/src/payment_driver.ts +167 -0
  45. package/src/payment_error.ts +159 -0
  46. package/src/payment_manager.ts +174 -0
  47. package/src/payment_provider.ts +93 -0
  48. package/src/tenant_metadata.ts +60 -0
  49. package/src/types.ts +49 -0
  50. package/src/webhook/index.ts +8 -0
  51. package/src/webhook/payment_webhook.ts +190 -0
  52. package/src/webhook/payment_webhook_event.ts +22 -0
  53. package/src/webhook/payment_webhook_event_repository.ts +65 -0
  54. package/src/webhook/payment_webhook_event_schema.ts +40 -0
  55. package/src/webhook/payment_webhook_registry.ts +65 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Omise webhook signature verification + event normalization.
3
+ *
4
+ * Omise signs webhook deliveries with HMAC SHA-256 over the raw
5
+ * body using the webhook secret from the Dashboard. The signature
6
+ * is sent in `X-Omise-Signature` as a hex digest. The SDK doesn't
7
+ * bundle a verifier — we implement it here.
8
+ *
9
+ * Event shape: `{ id, object: 'event', key, created_at, data }`
10
+ * where `key` is the event name (`'charge.complete'`,
11
+ * `'customer.create'`, …). We map the common keys onto the
12
+ * framework's `PaymentEventType` union.
13
+ */
14
+
15
+ import { createHmac, timingSafeEqual } from 'node:crypto'
16
+ import { WebhookSignatureError } from '../../payment_error.ts'
17
+ import type {
18
+ NormalizedWebhookEvent,
19
+ PaymentEventType,
20
+ } from '../../dto/payment_event.ts'
21
+ import { readTenantId } from '../../tenant_metadata.ts'
22
+ import {
23
+ toPaymentCharge,
24
+ toPaymentCustomer,
25
+ type OmiseCharge,
26
+ type OmiseCustomer,
27
+ } from './omise_mappers.ts'
28
+ import {
29
+ toPaymentSubscription as toPaymentSubscriptionFromSchedule,
30
+ type OmiseSchedule,
31
+ } from './omise_schedule_mapper.ts'
32
+
33
+ interface OmiseEvent {
34
+ id: string
35
+ object: 'event'
36
+ key: string
37
+ created_at?: string
38
+ created?: string
39
+ data: { object?: unknown; [k: string]: unknown }
40
+ }
41
+
42
+ const TYPE_MAP: Record<string, PaymentEventType> = {
43
+ 'customer.create': 'customer.created',
44
+ 'customer.update': 'customer.updated',
45
+ 'customer.destroy': 'customer.deleted',
46
+ 'charge.create': 'charge.succeeded',
47
+ 'charge.complete': 'charge.succeeded',
48
+ 'charge.update': 'charge.succeeded',
49
+ 'charge.capture': 'charge.succeeded',
50
+ 'charge.expire': 'charge.failed',
51
+ 'refund.create': 'charge.refunded',
52
+ 'schedule.create': 'subscription.created',
53
+ 'schedule.update': 'subscription.updated',
54
+ 'schedule.destroy': 'subscription.canceled',
55
+ }
56
+
57
+ /**
58
+ * Verify an Omise webhook signature against the raw body using
59
+ * the configured secret. Throws `WebhookSignatureError` on
60
+ * mismatch or missing secret. Returns the parsed event payload
61
+ * on success.
62
+ */
63
+ export async function omiseVerify(
64
+ rawBody: string,
65
+ signature: string,
66
+ webhookSecret: string | undefined,
67
+ ): Promise<OmiseEvent> {
68
+ if (!webhookSecret) {
69
+ throw new WebhookSignatureError(
70
+ 'OmisePaymentDriver.webhook.verify: `webhookSecret` is not set on the provider config.',
71
+ )
72
+ }
73
+ const expected = createHmac('sha256', webhookSecret).update(rawBody).digest('hex')
74
+ const provided = signature.trim()
75
+ // Use Uint8Array views to dodge the Buffer<ArrayBufferLike>
76
+ // mismatch with `timingSafeEqual`'s `ArrayBufferView`-typed
77
+ // params. Hex strings of unequal length are always rejected.
78
+ const expectedBuf = new Uint8Array(Buffer.from(expected, 'hex'))
79
+ const providedBuf = new Uint8Array(Buffer.from(provided, 'hex'))
80
+ if (
81
+ expectedBuf.length === 0 ||
82
+ expectedBuf.length !== providedBuf.length ||
83
+ !timingSafeEqual(expectedBuf, providedBuf)
84
+ ) {
85
+ throw new WebhookSignatureError(
86
+ 'OmisePaymentDriver.webhook.verify: signature mismatch.',
87
+ )
88
+ }
89
+ try {
90
+ return JSON.parse(rawBody) as OmiseEvent
91
+ } catch (cause) {
92
+ throw new WebhookSignatureError(
93
+ 'OmisePaymentDriver.webhook.verify: body is not valid JSON.',
94
+ { cause },
95
+ )
96
+ }
97
+ }
98
+
99
+ export function omiseNormalize(event: OmiseEvent): NormalizedWebhookEvent | null {
100
+ const type = TYPE_MAP[event.key]
101
+ if (!type) return null
102
+ const data: NormalizedWebhookEvent['data'] = {}
103
+ let fields: Record<string, unknown> | undefined
104
+ const object = event.data.object
105
+
106
+ switch (type) {
107
+ case 'customer.created':
108
+ case 'customer.updated':
109
+ if (object && typeof object === 'object') {
110
+ const dto = toPaymentCustomer(object as OmiseCustomer)
111
+ data.customerId = dto.id
112
+ fields = { ...dto }
113
+ }
114
+ break
115
+ case 'customer.deleted':
116
+ if (object && typeof object === 'object' && 'id' in (object as { id?: string })) {
117
+ data.customerId = (object as { id: string }).id
118
+ }
119
+ break
120
+ case 'charge.succeeded':
121
+ case 'charge.failed':
122
+ case 'charge.refunded':
123
+ if (object && typeof object === 'object') {
124
+ const dto = toPaymentCharge(object as OmiseCharge)
125
+ data.chargeId = dto.id
126
+ if (dto.customerId) data.customerId = dto.customerId
127
+ }
128
+ break
129
+ case 'subscription.created':
130
+ case 'subscription.updated':
131
+ case 'subscription.canceled':
132
+ if (object && typeof object === 'object') {
133
+ const dto = toPaymentSubscriptionFromSchedule(object as OmiseSchedule)
134
+ data.subscriptionId = dto.id
135
+ if (dto.customerId) data.customerId = dto.customerId
136
+ fields = { ...dto }
137
+ }
138
+ break
139
+ }
140
+
141
+ // Same convention as Stripe: read `strav_tenant_id` off the
142
+ // event's underlying resource metadata. Omise echoes metadata
143
+ // verbatim on every event the framework cares about.
144
+ const resourceMeta =
145
+ (object as { metadata?: Record<string, unknown> } | undefined)?.metadata ?? null
146
+ const tenantId = readTenantId(resourceMeta)
147
+
148
+ const normalized: NormalizedWebhookEvent = {
149
+ id: event.id,
150
+ type,
151
+ provider: 'omise',
152
+ raw: event,
153
+ data,
154
+ ...(tenantId ? { tenantId } : {}),
155
+ }
156
+ if (fields) {
157
+ ;(normalized as { _fields?: unknown })._fields = fields
158
+ }
159
+ return normalized
160
+ }
161
+
162
+ export type { OmiseEvent }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Helpers for handling `CreateChargeInput.paymentMethod` —
3
+ * `string | PaymentMethodSpec`.
4
+ *
5
+ * `extractCardToken` collapses both back-compat shapes (raw
6
+ * tokenized id string, `{ kind: 'card', token }` spec) onto a
7
+ * single token id. Specs of any other kind are not card flows;
8
+ * the helper signals that with `null`. Drivers then decide
9
+ * whether to route into their async-method pipeline (slices
10
+ * 7.2 / 7.3) or throw `ProviderUnsupportedError`.
11
+ *
12
+ * Drivers that don't yet support async methods can:
13
+ * const token = extractCardToken(input.paymentMethod)
14
+ * if (input.paymentMethod && !token) throw new ProviderUnsupportedError(...)
15
+ * // …pass `token` as before
16
+ */
17
+
18
+ import type { PaymentMethodSpec } from '../dto/payment_charge.ts'
19
+
20
+ export function extractCardToken(
21
+ pm: string | PaymentMethodSpec | undefined,
22
+ ): string | null {
23
+ if (pm === undefined) return null
24
+ if (typeof pm === 'string') return pm
25
+ if (pm.kind === 'card') return pm.token
26
+ return null
27
+ }
28
+
29
+ export function paymentMethodKind(
30
+ pm: string | PaymentMethodSpec | undefined,
31
+ ): PaymentMethodSpec['kind'] | 'unspecified' {
32
+ if (pm === undefined) return 'unspecified'
33
+ if (typeof pm === 'string') return 'card'
34
+ return pm.kind
35
+ }
@@ -0,0 +1,40 @@
1
+ // Public API of `@strav/payment/stripe`.
2
+ //
3
+ // Subpath barrel for the Stripe driver. Apps import from here to
4
+ // register the adapter:
5
+ //
6
+ // ```ts
7
+ // import { StripePaymentProvider } from '@strav/payment/stripe'
8
+ //
9
+ // export default [PaymentProvider, StripePaymentProvider, ...]
10
+ // ```
11
+ //
12
+ // `StripePaymentDriver` + mapper exports are advanced — used by
13
+ // tests and by apps that hand-wire a driver instance via
14
+ // `manager.useDriver(name, driver)`.
15
+
16
+ export {
17
+ toPaymentCharge,
18
+ toPaymentCheckoutSession,
19
+ toPaymentCustomer,
20
+ toPaymentInvoice,
21
+ toPaymentLink,
22
+ toPaymentMethod,
23
+ toPaymentPrice,
24
+ toPaymentProduct,
25
+ toPaymentSubscription,
26
+ } from './mappers/stripe_mappers.ts'
27
+ export {
28
+ buildStripeMethodWiring,
29
+ STRIPE_SUPPORTED_METHOD_KINDS,
30
+ type StripeMethodBuildResult,
31
+ type StripeMethodWiring,
32
+ } from './mappers/stripe_method_spec.ts'
33
+ export { stripeNextAction } from './mappers/stripe_next_action_mapper.ts'
34
+ export type { StripeProviderConfig } from './stripe_config.ts'
35
+ export {
36
+ StripePaymentDriver,
37
+ type StripeDriverOptions,
38
+ } from './stripe_driver.ts'
39
+ export { StripePaymentProvider } from './stripe_provider.ts'
40
+ export { stripeNormalize } from './webhook/stripe_normalize.ts'
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Stripe ↔ normalized-DTO mappers. One function per resource;
3
+ * each converts a `Stripe.<X>` SDK object into the framework's
4
+ * `Payment<X>` DTO with the native object on `.raw`.
5
+ *
6
+ * Field name conventions:
7
+ * - Stripe timestamps are unix seconds; we multiply by 1000.
8
+ * - Stripe metadata is `Record<string, string>` directly.
9
+ * - Missing-from-Stripe-but-required-by-DTO falls back to
10
+ * sensible defaults (empty string, `{}`); never invent ids.
11
+ */
12
+
13
+ import type Stripe from 'stripe'
14
+ import type {
15
+ ChargeStatus,
16
+ InvoiceStatus,
17
+ PaymentCharge,
18
+ PaymentCheckoutSession,
19
+ PaymentCustomer,
20
+ PaymentInvoice,
21
+ PaymentMethod,
22
+ PaymentPrice,
23
+ PaymentProduct,
24
+ PaymentSubscription,
25
+ SubscriptionStatus,
26
+ } from '../../../dto/index.ts'
27
+
28
+ const PROVIDER = 'stripe'
29
+
30
+ function toDate(unix: number | null | undefined): Date | null {
31
+ if (unix === null || unix === undefined) return null
32
+ return new Date(unix * 1000)
33
+ }
34
+
35
+ function metadata(m: Stripe.Metadata | null | undefined): Record<string, string> {
36
+ if (!m) return {}
37
+ const out: Record<string, string> = {}
38
+ for (const [k, v] of Object.entries(m)) {
39
+ if (v === null) continue
40
+ out[k] = String(v)
41
+ }
42
+ return out
43
+ }
44
+
45
+ export function toPaymentCustomer(c: Stripe.Customer): PaymentCustomer {
46
+ return {
47
+ id: c.id,
48
+ provider: PROVIDER,
49
+ email: c.email ?? '',
50
+ ...(c.name ? { name: c.name } : {}),
51
+ ...(c.phone ? { phone: c.phone } : {}),
52
+ metadata: metadata(c.metadata),
53
+ createdAt: new Date(c.created * 1000),
54
+ raw: c,
55
+ }
56
+ }
57
+
58
+ export function toPaymentProduct(p: Stripe.Product): PaymentProduct {
59
+ return {
60
+ id: p.id,
61
+ provider: PROVIDER,
62
+ name: p.name,
63
+ ...(p.description ? { description: p.description } : {}),
64
+ active: p.active,
65
+ metadata: metadata(p.metadata),
66
+ createdAt: new Date(p.created * 1000),
67
+ raw: p,
68
+ }
69
+ }
70
+
71
+ export function toPaymentPrice(p: Stripe.Price): PaymentPrice {
72
+ return {
73
+ id: p.id,
74
+ provider: PROVIDER,
75
+ productId: typeof p.product === 'string' ? p.product : (p.product as { id: string }).id,
76
+ amount: p.unit_amount ?? 0,
77
+ currency: p.currency,
78
+ type: p.type === 'recurring' ? 'recurring' : 'one_time',
79
+ ...(p.recurring?.interval
80
+ ? { interval: p.recurring.interval as PaymentPrice['interval'] }
81
+ : {}),
82
+ ...(p.recurring?.interval_count
83
+ ? { intervalCount: p.recurring.interval_count }
84
+ : {}),
85
+ active: p.active,
86
+ metadata: metadata(p.metadata),
87
+ createdAt: new Date(p.created * 1000),
88
+ raw: p,
89
+ }
90
+ }
91
+
92
+ const SUBSCRIPTION_STATUS_MAP: Record<Stripe.Subscription.Status, SubscriptionStatus> = {
93
+ active: 'active',
94
+ trialing: 'trialing',
95
+ past_due: 'past_due',
96
+ canceled: 'canceled',
97
+ unpaid: 'past_due',
98
+ incomplete: 'incomplete',
99
+ incomplete_expired: 'canceled',
100
+ paused: 'paused',
101
+ }
102
+
103
+ export function toPaymentSubscription(s: Stripe.Subscription): PaymentSubscription {
104
+ // Stripe sometimes nests the actual price on items.data[0].price.
105
+ const firstItem = s.items.data[0]
106
+ const priceId = firstItem
107
+ ? typeof firstItem.price === 'string'
108
+ ? firstItem.price
109
+ : firstItem.price.id
110
+ : ''
111
+ // `current_period_start/end` moved to the item level in recent API
112
+ // versions; fall back across both shapes.
113
+ const sAny = s as unknown as { current_period_start?: number; current_period_end?: number }
114
+ const itemAny = firstItem as unknown as
115
+ | { current_period_start?: number; current_period_end?: number }
116
+ | undefined
117
+ const periodStart = sAny.current_period_start ?? itemAny?.current_period_start ?? s.start_date
118
+ const periodEnd = sAny.current_period_end ?? itemAny?.current_period_end ?? s.start_date
119
+ return {
120
+ id: s.id,
121
+ provider: PROVIDER,
122
+ customerId: typeof s.customer === 'string' ? s.customer : (s.customer as { id: string }).id,
123
+ priceId,
124
+ status: SUBSCRIPTION_STATUS_MAP[s.status] ?? 'active',
125
+ currentPeriodStart: new Date(periodStart * 1000),
126
+ currentPeriodEnd: new Date(periodEnd * 1000),
127
+ cancelAt: toDate(s.cancel_at),
128
+ canceledAt: toDate(s.canceled_at),
129
+ trialStart: toDate(s.trial_start),
130
+ trialEnd: toDate(s.trial_end),
131
+ metadata: metadata(s.metadata),
132
+ createdAt: new Date(s.created * 1000),
133
+ raw: s,
134
+ }
135
+ }
136
+
137
+ function paymentMethodKind(kind: string): PaymentMethod['kind'] {
138
+ switch (kind) {
139
+ case 'card':
140
+ return 'card'
141
+ case 'us_bank_account':
142
+ case 'bank_account':
143
+ return 'bank_account'
144
+ case 'sepa_debit':
145
+ return 'sepa_debit'
146
+ case 'paypal':
147
+ return 'paypal'
148
+ default:
149
+ return 'other'
150
+ }
151
+ }
152
+
153
+ export function toPaymentMethod(pm: Stripe.PaymentMethod): PaymentMethod {
154
+ const card = pm.card
155
+ return {
156
+ id: pm.id,
157
+ provider: PROVIDER,
158
+ customerId:
159
+ typeof pm.customer === 'string'
160
+ ? pm.customer
161
+ : pm.customer
162
+ ? (pm.customer as { id: string }).id
163
+ : null,
164
+ kind: paymentMethodKind(pm.type),
165
+ ...(card?.brand ? { brand: card.brand } : {}),
166
+ ...(card?.last4 ? { last4: card.last4 } : {}),
167
+ ...(card?.exp_month ? { expMonth: card.exp_month } : {}),
168
+ ...(card?.exp_year ? { expYear: card.exp_year } : {}),
169
+ metadata: metadata(pm.metadata),
170
+ createdAt: new Date(pm.created * 1000),
171
+ raw: pm,
172
+ }
173
+ }
174
+
175
+ const CHARGE_STATUS_MAP: Record<Stripe.Charge.Status, ChargeStatus> = {
176
+ succeeded: 'succeeded',
177
+ pending: 'pending',
178
+ failed: 'failed',
179
+ }
180
+
181
+ export function toPaymentCharge(c: Stripe.Charge): PaymentCharge {
182
+ const status: ChargeStatus = c.refunded
183
+ ? c.amount_refunded === c.amount
184
+ ? 'refunded'
185
+ : 'partial_refunded'
186
+ : (CHARGE_STATUS_MAP[c.status] ?? 'pending')
187
+ return {
188
+ id: c.id,
189
+ provider: PROVIDER,
190
+ customerId: typeof c.customer === 'string' ? c.customer : (c.customer as { id: string } | null)?.id ?? null,
191
+ amount: c.amount,
192
+ currency: c.currency,
193
+ status,
194
+ paymentMethodId:
195
+ typeof c.payment_method === 'string'
196
+ ? c.payment_method
197
+ : (c.payment_method as { id: string } | null)?.id ?? null,
198
+ failureCode: c.failure_code,
199
+ failureMessage: c.failure_message,
200
+ // Settled charges don't carry a next-action. The intent-level
201
+ // mapper used by `charges.create` populates `nextAction` from
202
+ // `PaymentIntent.next_action` when the charge is still in
203
+ // `requires_action`; that lands in slice 7.2.
204
+ nextAction: null,
205
+ metadata: metadata(c.metadata),
206
+ createdAt: new Date(c.created * 1000),
207
+ raw: c,
208
+ }
209
+ }
210
+
211
+ const INVOICE_STATUS_MAP: Record<NonNullable<Stripe.Invoice.Status>, InvoiceStatus> = {
212
+ draft: 'draft',
213
+ open: 'open',
214
+ paid: 'paid',
215
+ uncollectible: 'uncollectible',
216
+ void: 'void',
217
+ }
218
+
219
+ export function toPaymentInvoice(i: Stripe.Invoice): PaymentInvoice {
220
+ // Stripe invoices carry the subscription reference on a nested
221
+ // line-item property; fall back to a top-level field on older
222
+ // shapes.
223
+ const iAny = i as unknown as {
224
+ subscription?: string | { id: string } | null
225
+ customer?: string | { id: string } | null
226
+ hosted_invoice_url?: string | null
227
+ invoice_pdf?: string | null
228
+ }
229
+ const subId =
230
+ typeof iAny.subscription === 'string'
231
+ ? iAny.subscription
232
+ : iAny.subscription
233
+ ? (iAny.subscription as { id: string }).id
234
+ : null
235
+ return {
236
+ id: i.id ?? '',
237
+ provider: PROVIDER,
238
+ customerId:
239
+ typeof iAny.customer === 'string'
240
+ ? iAny.customer
241
+ : iAny.customer
242
+ ? (iAny.customer as { id: string }).id
243
+ : '',
244
+ subscriptionId: subId,
245
+ status: i.status ? (INVOICE_STATUS_MAP[i.status] ?? 'open') : 'draft',
246
+ amount: i.amount_due,
247
+ amountPaid: i.amount_paid,
248
+ amountDue: i.amount_remaining ?? i.amount_due,
249
+ currency: i.currency,
250
+ hostedUrl: iAny.hosted_invoice_url ?? null,
251
+ pdfUrl: iAny.invoice_pdf ?? null,
252
+ dueAt: toDate(i.due_date),
253
+ paidAt: i.status === 'paid' ? toDate(i.status_transitions?.paid_at) : null,
254
+ metadata: metadata(i.metadata),
255
+ createdAt: new Date(i.created * 1000),
256
+ raw: i,
257
+ }
258
+ }
259
+
260
+ export function toPaymentLink(l: Stripe.PaymentLink): import('../../../dto/index.ts').PaymentLink {
261
+ // Stripe Payment Links carry their amount/currency on the
262
+ // attached line_items.data[0].price — when the SDK doesn't
263
+ // expand it, those fields are `null` on our DTO and apps
264
+ // resolve the price separately. The link itself doesn't
265
+ // expose a top-level amount.
266
+ return {
267
+ id: l.id,
268
+ provider: PROVIDER,
269
+ url: l.url,
270
+ amount: null,
271
+ currency: null,
272
+ active: l.active,
273
+ reusable: true, // Stripe links are reusable by default; single-use is rare.
274
+ metadata: metadata(l.metadata),
275
+ createdAt: new Date(0), // PaymentLink.created not on Stripe.PaymentLink — apps that need it read from the dashboard.
276
+ raw: l,
277
+ }
278
+ }
279
+
280
+ export function toPaymentCheckoutSession(
281
+ s: Stripe.Checkout.Session,
282
+ ): PaymentCheckoutSession {
283
+ return {
284
+ id: s.id,
285
+ provider: PROVIDER,
286
+ mode: s.mode === 'subscription' ? 'subscription' : s.mode === 'setup' ? 'setup' : 'payment',
287
+ status: s.status === 'complete' ? 'complete' : s.status === 'expired' ? 'expired' : 'open',
288
+ url: s.url ?? '',
289
+ customerId:
290
+ typeof s.customer === 'string'
291
+ ? s.customer
292
+ : s.customer
293
+ ? (s.customer as { id: string }).id
294
+ : null,
295
+ paymentIntentId:
296
+ typeof s.payment_intent === 'string'
297
+ ? s.payment_intent
298
+ : s.payment_intent
299
+ ? (s.payment_intent as { id: string }).id
300
+ : null,
301
+ subscriptionId:
302
+ typeof s.subscription === 'string'
303
+ ? s.subscription
304
+ : s.subscription
305
+ ? (s.subscription as { id: string }).id
306
+ : null,
307
+ expiresAt: toDate(s.expires_at),
308
+ metadata: metadata(s.metadata),
309
+ createdAt: new Date(s.created * 1000),
310
+ raw: s,
311
+ }
312
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Build a Stripe `payment_method_data` (plus matching
3
+ * `payment_method_options` extras) from a `PaymentMethodSpec`.
4
+ *
5
+ * Returns `null` when the spec is a card token (the caller passes
6
+ * `payment_method: <token>` directly), or `undefined` when the
7
+ * driver should reject the kind via `ProviderUnsupportedError`.
8
+ *
9
+ * Stripe supports a different set of methods than Omise — this
10
+ * function is the single place where the mapping lives so the
11
+ * capability set + the create-call wiring stay in sync.
12
+ */
13
+
14
+ import type Stripe from 'stripe'
15
+ import type { PaymentMethodSpec } from '../../../dto/index.ts'
16
+
17
+ export interface StripeMethodWiring {
18
+ payment_method_data: Stripe.PaymentIntentCreateParams.PaymentMethodData
19
+ payment_method_options?: Stripe.PaymentIntentCreateParams.PaymentMethodOptions
20
+ }
21
+
22
+ export type StripeMethodBuildResult =
23
+ | { kind: 'card_token' }
24
+ | { kind: 'unsupported' }
25
+ | { kind: 'wired'; wiring: StripeMethodWiring }
26
+
27
+ /**
28
+ * Closed set of `PaymentMethodSpec.kind` values Stripe accepts.
29
+ * `card` is handled separately (caller passes the token id);
30
+ * everything else routes through `payment_method_data.type`.
31
+ */
32
+ const STRIPE_TYPES: Partial<Record<PaymentMethodSpec['kind'], string>> = {
33
+ promptpay: 'promptpay',
34
+ paynow: 'paynow',
35
+ alipay: 'alipay',
36
+ wechat_pay: 'wechat_pay',
37
+ grabpay: 'grabpay',
38
+ kakaopay: 'kakao_pay',
39
+ konbini: 'konbini',
40
+ }
41
+
42
+ export function buildStripeMethodWiring(
43
+ spec: PaymentMethodSpec,
44
+ ): StripeMethodBuildResult {
45
+ if (spec.kind === 'card') return { kind: 'card_token' }
46
+ const stripeType = STRIPE_TYPES[spec.kind]
47
+ if (!stripeType) return { kind: 'unsupported' }
48
+
49
+ const data = { type: stripeType } as Stripe.PaymentIntentCreateParams.PaymentMethodData
50
+
51
+ const wiring: StripeMethodWiring = { payment_method_data: data }
52
+
53
+ // Per-kind extras. WeChat needs a client hint; Konbini takes
54
+ // a confirmation_number setting via payment_method_options.
55
+ if (spec.kind === 'wechat_pay') {
56
+ wiring.payment_method_options = {
57
+ wechat_pay: { client: 'web' },
58
+ } as Stripe.PaymentIntentCreateParams.PaymentMethodOptions
59
+ }
60
+
61
+ return { kind: 'wired', wiring }
62
+ }
63
+
64
+ /**
65
+ * The `PaymentCapability` flag corresponding to a spec kind. Used
66
+ * by the driver to declare its supported set + by tests.
67
+ */
68
+ export const STRIPE_SUPPORTED_METHOD_KINDS: ReadonlyArray<PaymentMethodSpec['kind']> = [
69
+ 'card',
70
+ 'promptpay',
71
+ 'paynow',
72
+ 'alipay',
73
+ 'wechat_pay',
74
+ 'grabpay',
75
+ 'kakaopay',
76
+ 'konbini',
77
+ ]