@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,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
+ }
@@ -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
+ }