@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,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `OmisePaymentDriver` — `PaymentDriver` for Omise (Opn Payments).
|
|
3
|
+
*
|
|
4
|
+
* Capability scope is intentionally narrower than Stripe:
|
|
5
|
+
*
|
|
6
|
+
* - **customers** full CRUD.
|
|
7
|
+
* - **charges** create / retrieve / capture / refund.
|
|
8
|
+
* No `update` (Omise charges are immutable
|
|
9
|
+
* beyond capture + refund).
|
|
10
|
+
* - **paymentMethods** list + detach (via cards on a customer).
|
|
11
|
+
* attach uses card tokens; apps create
|
|
12
|
+
* tokens client-side via Omise.js and
|
|
13
|
+
* pass the token id here.
|
|
14
|
+
* - **subscriptions** create / retrieve / cancel / list (via
|
|
15
|
+
* customer). Backed by Omise's schedules
|
|
16
|
+
* API. `update` throws — Omise schedules
|
|
17
|
+
* are immutable. The framework `price`
|
|
18
|
+
* field carries an `omise_spec:…` blob
|
|
19
|
+
* built by `omisePriceSpec({...})`;
|
|
20
|
+
* Omise has no separate price catalogue,
|
|
21
|
+
* so the spec encodes amount + currency +
|
|
22
|
+
* period inline.
|
|
23
|
+
*
|
|
24
|
+
* - **products / prices / invoices / checkout** throw
|
|
25
|
+
* `ProviderUnsupportedError`. Omise has sources + payment
|
|
26
|
+
* links but they don't map cleanly onto the framework's
|
|
27
|
+
* Stripe-flavored union in v1.
|
|
28
|
+
*
|
|
29
|
+
* Apps that need product/price catalogs alongside Omise charges
|
|
30
|
+
* use a separate Stripe provider entry just for the catalog and
|
|
31
|
+
* route by `payment.use(name)`.
|
|
32
|
+
*
|
|
33
|
+
* Webhook signature: HMAC SHA-256 over the raw body, `X-Omise-
|
|
34
|
+
* Signature` header. Implementation in `omise_webhook.ts`.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// biome-ignore lint/style/useImportType: Omise is a CJS value import.
|
|
38
|
+
import Omise from 'omise'
|
|
39
|
+
import type { PaymentCapability } from '../../payment_capabilities.ts'
|
|
40
|
+
import type {
|
|
41
|
+
ChargeOps,
|
|
42
|
+
CheckoutOps,
|
|
43
|
+
CustomerOps,
|
|
44
|
+
InvoiceOps,
|
|
45
|
+
LinkOps,
|
|
46
|
+
PaymentDriver,
|
|
47
|
+
PaymentMethodOps,
|
|
48
|
+
PriceOps,
|
|
49
|
+
ProductOps,
|
|
50
|
+
SubscriptionOps,
|
|
51
|
+
WebhookOps,
|
|
52
|
+
} from '../../payment_driver.ts'
|
|
53
|
+
import { extractCardToken, paymentMethodKind } from '../payment_method_helpers.ts'
|
|
54
|
+
import { ProviderUnsupportedError } from '../../payment_error.ts'
|
|
55
|
+
import type {
|
|
56
|
+
CancelSubscriptionOptions,
|
|
57
|
+
CreateChargeInput,
|
|
58
|
+
CreateCustomerInput,
|
|
59
|
+
CreatePaymentLinkInput,
|
|
60
|
+
CreateRefundInput,
|
|
61
|
+
CreateSubscriptionInput,
|
|
62
|
+
ListCustomersOptions,
|
|
63
|
+
ListPaymentLinksOptions,
|
|
64
|
+
ListPaymentMethodsOptions,
|
|
65
|
+
ListSubscriptionsOptions,
|
|
66
|
+
NormalizedWebhookEvent,
|
|
67
|
+
PaginatedCustomers,
|
|
68
|
+
PaginatedPaymentLinks,
|
|
69
|
+
PaginatedPaymentMethods,
|
|
70
|
+
PaginatedSubscriptions,
|
|
71
|
+
PaymentCharge,
|
|
72
|
+
PaymentCustomer,
|
|
73
|
+
PaymentLink,
|
|
74
|
+
PaymentMethod,
|
|
75
|
+
PaymentRefund,
|
|
76
|
+
PaymentSubscription,
|
|
77
|
+
UpdateCustomerInput,
|
|
78
|
+
} from '../../dto/index.ts'
|
|
79
|
+
import {
|
|
80
|
+
toPaymentCharge,
|
|
81
|
+
toPaymentCustomer,
|
|
82
|
+
toPaymentLink,
|
|
83
|
+
toPaymentMethod,
|
|
84
|
+
type OmiseCard,
|
|
85
|
+
type OmiseCharge,
|
|
86
|
+
type OmiseCustomer,
|
|
87
|
+
type OmiseLink,
|
|
88
|
+
type OmiseSource,
|
|
89
|
+
} from './omise_mappers.ts'
|
|
90
|
+
import {
|
|
91
|
+
buildOmiseMethodSpec,
|
|
92
|
+
OMISE_SUPPORTED_METHOD_KINDS,
|
|
93
|
+
} from './omise_method_spec.ts'
|
|
94
|
+
import type { OmiseProviderConfig } from './omise_config.ts'
|
|
95
|
+
import { parseOmisePriceSpec } from './omise_price_spec.ts'
|
|
96
|
+
import {
|
|
97
|
+
toPaymentSubscription as toPaymentSubscriptionFromSchedule,
|
|
98
|
+
type OmiseSchedule,
|
|
99
|
+
} from './omise_schedule_mapper.ts'
|
|
100
|
+
import { omiseNormalize, omiseVerify, type OmiseEvent } from './omise_webhook.ts'
|
|
101
|
+
|
|
102
|
+
const PROVIDER = 'omise'
|
|
103
|
+
|
|
104
|
+
const CAPS: readonly PaymentCapability[] = [
|
|
105
|
+
'customers.create', 'customers.update', 'customers.retrieve', 'customers.list', 'customers.delete',
|
|
106
|
+
'paymentMethods.attach', 'paymentMethods.detach', 'paymentMethods.list',
|
|
107
|
+
'charges.create', 'charges.refund', 'charges.capture',
|
|
108
|
+
// Async payment methods backed by Omise Sources. PromptPay is
|
|
109
|
+
// the only QR-based one in this list; the rest are redirect
|
|
110
|
+
// flows. Stripe-only kinds (`paynow`, `kakaopay`, `konbini`,
|
|
111
|
+
// `fps`) throw — Omise's regional fit is TH / SEA wallets.
|
|
112
|
+
...OMISE_SUPPORTED_METHOD_KINDS.map(
|
|
113
|
+
(k) => `charges.method.${k}` as PaymentCapability,
|
|
114
|
+
),
|
|
115
|
+
'charges.nextAction.display_qr',
|
|
116
|
+
'charges.nextAction.redirect',
|
|
117
|
+
'charges.nextAction.wait',
|
|
118
|
+
// Omise schedules: subscriptions.create / retrieve / cancel / list-by-customer.
|
|
119
|
+
// `update` and `changePlan` aren't supported — Omise schedules are immutable.
|
|
120
|
+
// `trials` aren't supported — schedules have no trial concept.
|
|
121
|
+
'subscriptions.create', 'subscriptions.retrieve', 'subscriptions.cancel',
|
|
122
|
+
// Payment Links — Omise supports create / retrieve / list. No
|
|
123
|
+
// deactivate endpoint, so `links.deactivate` throws.
|
|
124
|
+
'links.create',
|
|
125
|
+
'webhook.verify', 'webhook.normalize',
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
export interface OmiseDriverOptions {
|
|
129
|
+
instanceName: string
|
|
130
|
+
config: OmiseProviderConfig
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface OmiseClient {
|
|
134
|
+
customers: {
|
|
135
|
+
create(req: Record<string, unknown>): Promise<OmiseCustomer>
|
|
136
|
+
retrieve(id: string): Promise<OmiseCustomer>
|
|
137
|
+
update(id: string, req: Record<string, unknown>): Promise<OmiseCustomer>
|
|
138
|
+
destroy(id: string): Promise<{ deleted: boolean }>
|
|
139
|
+
list(params?: { limit?: number; offset?: number }): Promise<{ data: OmiseCustomer[]; total: number }>
|
|
140
|
+
listCards(
|
|
141
|
+
customerID: string,
|
|
142
|
+
params?: { limit?: number; offset?: number },
|
|
143
|
+
): Promise<{ data: OmiseCard[] }>
|
|
144
|
+
destroyCard(customerID: string, cardID: string): Promise<OmiseCard>
|
|
145
|
+
schedules(
|
|
146
|
+
customerID: string,
|
|
147
|
+
params?: { limit?: number; offset?: number },
|
|
148
|
+
): Promise<{ data: OmiseSchedule[] }>
|
|
149
|
+
}
|
|
150
|
+
charges: {
|
|
151
|
+
create(req: Record<string, unknown>): Promise<OmiseCharge>
|
|
152
|
+
retrieve(id: string): Promise<OmiseCharge>
|
|
153
|
+
capture(id: string): Promise<OmiseCharge>
|
|
154
|
+
createRefund(id: string, req: Record<string, unknown>): Promise<{
|
|
155
|
+
id: string
|
|
156
|
+
amount: number
|
|
157
|
+
currency: string
|
|
158
|
+
charge: string
|
|
159
|
+
created?: string
|
|
160
|
+
created_at?: string
|
|
161
|
+
voided?: boolean
|
|
162
|
+
}>
|
|
163
|
+
}
|
|
164
|
+
schedules: {
|
|
165
|
+
create(req: Record<string, unknown>): Promise<OmiseSchedule>
|
|
166
|
+
retrieve(id: string): Promise<OmiseSchedule>
|
|
167
|
+
destroy(id: string): Promise<{ deleted: boolean } | OmiseSchedule>
|
|
168
|
+
}
|
|
169
|
+
sources: {
|
|
170
|
+
create(req: Record<string, unknown>): Promise<OmiseSource>
|
|
171
|
+
retrieve(id: string): Promise<OmiseSource>
|
|
172
|
+
}
|
|
173
|
+
links: {
|
|
174
|
+
create(req: Record<string, unknown>): Promise<OmiseLink>
|
|
175
|
+
retrieve(id: string): Promise<OmiseLink>
|
|
176
|
+
list(params?: { limit?: number; offset?: number }): Promise<{ data: OmiseLink[] }>
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function uns(op: string, reason: string): (...args: unknown[]) => never {
|
|
181
|
+
return () => {
|
|
182
|
+
throw new ProviderUnsupportedError(PROVIDER, op, { reason })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function todayIso(): string {
|
|
187
|
+
return new Date().toISOString().slice(0, 10)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function oneYearFromIso(isoDay: string): string {
|
|
191
|
+
const [y, m, d] = isoDay.split('-').map(Number) as [number, number, number]
|
|
192
|
+
// Roll forward one year. JS handles month/day correctly; leap-year
|
|
193
|
+
// Feb 29 wraps to Feb 28, which is fine for a schedule end-date.
|
|
194
|
+
return new Date(Date.UTC(y + 1, m - 1, d)).toISOString().slice(0, 10)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export class OmisePaymentDriver implements PaymentDriver {
|
|
198
|
+
readonly name = PROVIDER
|
|
199
|
+
readonly instanceName: string
|
|
200
|
+
readonly capabilities: ReadonlySet<PaymentCapability> = new Set(CAPS)
|
|
201
|
+
|
|
202
|
+
readonly client: OmiseClient
|
|
203
|
+
private readonly config: OmiseProviderConfig
|
|
204
|
+
|
|
205
|
+
constructor(options: OmiseDriverOptions) {
|
|
206
|
+
this.instanceName = options.instanceName
|
|
207
|
+
this.config = options.config
|
|
208
|
+
this.client =
|
|
209
|
+
(options.config.client as OmiseClient | undefined) ??
|
|
210
|
+
(Omise({
|
|
211
|
+
publicKey: options.config.publicKey,
|
|
212
|
+
secretKey: options.config.secretKey,
|
|
213
|
+
...(options.config.omiseVersion ? { omiseVersion: options.config.omiseVersion } : {}),
|
|
214
|
+
}) as unknown as OmiseClient)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
readonly customers: CustomerOps = {
|
|
218
|
+
create: async (input: CreateCustomerInput): Promise<PaymentCustomer> => {
|
|
219
|
+
const c = await this.client.customers.create({
|
|
220
|
+
email: input.email,
|
|
221
|
+
...(input.name !== undefined ? { description: input.name } : {}),
|
|
222
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
223
|
+
})
|
|
224
|
+
return toPaymentCustomer(c)
|
|
225
|
+
},
|
|
226
|
+
retrieve: async (id: string): Promise<PaymentCustomer> => {
|
|
227
|
+
return toPaymentCustomer(await this.client.customers.retrieve(id))
|
|
228
|
+
},
|
|
229
|
+
update: async (id: string, input: UpdateCustomerInput): Promise<PaymentCustomer> => {
|
|
230
|
+
const c = await this.client.customers.update(id, {
|
|
231
|
+
...(input.email !== undefined ? { email: input.email } : {}),
|
|
232
|
+
...(input.name !== undefined ? { description: input.name } : {}),
|
|
233
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
234
|
+
})
|
|
235
|
+
return toPaymentCustomer(c)
|
|
236
|
+
},
|
|
237
|
+
list: async (options: ListCustomersOptions = {}): Promise<PaginatedCustomers> => {
|
|
238
|
+
const page = await this.client.customers.list({
|
|
239
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
240
|
+
})
|
|
241
|
+
// Omise pagination is offset-based — apps that need next-page
|
|
242
|
+
// fetch carry an offset in their own state; we don't surface
|
|
243
|
+
// a cursor for v1.
|
|
244
|
+
const filtered = options.email
|
|
245
|
+
? page.data.filter((c: OmiseCustomer) => c.email === options.email)
|
|
246
|
+
: page.data
|
|
247
|
+
return {
|
|
248
|
+
data: filtered.map(toPaymentCustomer),
|
|
249
|
+
nextCursor: null,
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
delete: async (id: string): Promise<void> => {
|
|
253
|
+
await this.client.customers.destroy(id)
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Catalog-style ops: not supported by Omise's flat-charge model ────
|
|
258
|
+
|
|
259
|
+
readonly products: ProductOps = {
|
|
260
|
+
create: uns('products.create', 'Omise has no Products catalog; pass amount + currency directly to charges.create.'),
|
|
261
|
+
retrieve: uns('products.retrieve', 'Omise has no Products catalog.'),
|
|
262
|
+
update: uns('products.update', 'Omise has no Products catalog.'),
|
|
263
|
+
list: uns('products.list', 'Omise has no Products catalog.'),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
readonly prices: PriceOps = {
|
|
267
|
+
create: uns('prices.create', 'Omise has no Prices catalog; pass amount + currency directly to charges.create.'),
|
|
268
|
+
retrieve: uns('prices.retrieve', 'Omise has no Prices catalog.'),
|
|
269
|
+
list: uns('prices.list', 'Omise has no Prices catalog.'),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
readonly subscriptions: SubscriptionOps = {
|
|
273
|
+
create: async (input: CreateSubscriptionInput): Promise<PaymentSubscription> => {
|
|
274
|
+
if (input.trialDays !== undefined) {
|
|
275
|
+
throw new ProviderUnsupportedError(
|
|
276
|
+
PROVIDER,
|
|
277
|
+
'subscriptions.trials',
|
|
278
|
+
{ reason: 'Omise schedules have no trial concept. Drop `trialDays` or use a one-off charge before the schedule starts.' },
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
const spec = parseOmisePriceSpec(input.price)
|
|
282
|
+
if (!spec) {
|
|
283
|
+
throw new ProviderUnsupportedError(
|
|
284
|
+
PROVIDER,
|
|
285
|
+
'subscriptions.create',
|
|
286
|
+
{ reason: 'Omise has no `price` catalog. Build the price inline with `omisePriceSpec({ amount, currency, period, every? })` and pass the result as `price`.' },
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
const startDate = todayIso()
|
|
290
|
+
const endDate = oneYearFromIso(startDate)
|
|
291
|
+
const charge: Record<string, unknown> = {
|
|
292
|
+
customer: input.customer,
|
|
293
|
+
amount: spec.amount,
|
|
294
|
+
currency: spec.currency,
|
|
295
|
+
...(spec.description ? { description: spec.description } : {}),
|
|
296
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
297
|
+
}
|
|
298
|
+
const cardId = input.paymentMethod ?? spec.card
|
|
299
|
+
if (cardId) charge.card = cardId
|
|
300
|
+
const schedule = await this.client.schedules.create({
|
|
301
|
+
every: spec.every ?? 1,
|
|
302
|
+
period: spec.period,
|
|
303
|
+
start_date: startDate,
|
|
304
|
+
end_date: endDate,
|
|
305
|
+
charge,
|
|
306
|
+
})
|
|
307
|
+
return toPaymentSubscriptionFromSchedule(schedule)
|
|
308
|
+
},
|
|
309
|
+
retrieve: async (id: string): Promise<PaymentSubscription> => {
|
|
310
|
+
return toPaymentSubscriptionFromSchedule(await this.client.schedules.retrieve(id))
|
|
311
|
+
},
|
|
312
|
+
update: uns(
|
|
313
|
+
'subscriptions.update',
|
|
314
|
+
'Omise schedules are immutable. Cancel the current schedule and create a new one with the updated terms.',
|
|
315
|
+
),
|
|
316
|
+
cancel: async (
|
|
317
|
+
id: string,
|
|
318
|
+
_options: CancelSubscriptionOptions = {},
|
|
319
|
+
): Promise<PaymentSubscription> => {
|
|
320
|
+
// Omise has no "cancel at period end" — destroy stops the
|
|
321
|
+
// schedule immediately. The `_options.at` argument is
|
|
322
|
+
// accepted for API uniformity but cannot change Omise's
|
|
323
|
+
// behaviour.
|
|
324
|
+
const result = await this.client.schedules.destroy(id)
|
|
325
|
+
if (result && typeof result === 'object' && 'id' in result) {
|
|
326
|
+
return toPaymentSubscriptionFromSchedule(result as OmiseSchedule)
|
|
327
|
+
}
|
|
328
|
+
// SDK returned `{ deleted: true }` — rehydrate via retrieve so
|
|
329
|
+
// we have the post-destroy state to return.
|
|
330
|
+
return toPaymentSubscriptionFromSchedule(await this.client.schedules.retrieve(id))
|
|
331
|
+
},
|
|
332
|
+
list: async (
|
|
333
|
+
options: ListSubscriptionsOptions = {},
|
|
334
|
+
): Promise<PaginatedSubscriptions> => {
|
|
335
|
+
if (!options.customer) {
|
|
336
|
+
throw new ProviderUnsupportedError(
|
|
337
|
+
PROVIDER,
|
|
338
|
+
'subscriptions.list',
|
|
339
|
+
{ reason: 'Omise only lists schedules per-customer. Pass `customer` to scope the listing.' },
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
const page = await this.client.customers.schedules(options.customer, {
|
|
343
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
344
|
+
})
|
|
345
|
+
const data = page.data.map(toPaymentSubscriptionFromSchedule)
|
|
346
|
+
const filtered = options.status
|
|
347
|
+
? data.filter((s) => s.status === options.status)
|
|
348
|
+
: data
|
|
349
|
+
return { data: filtered, nextCursor: null }
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
readonly invoices: InvoiceOps = {
|
|
354
|
+
retrieve: uns('invoices.retrieve', 'Omise has no invoices.'),
|
|
355
|
+
list: uns('invoices.list', 'Omise has no invoices.'),
|
|
356
|
+
finalize: uns('invoices.finalize', 'Omise has no invoices.'),
|
|
357
|
+
void: uns('invoices.void', 'Omise has no invoices.'),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
readonly checkout: CheckoutOps = {
|
|
361
|
+
create: uns('checkout.create', 'Omise uses Payment Links instead of multi-mode hosted checkout; not bridged in v1.'),
|
|
362
|
+
retrieve: uns('checkout.retrieve', 'Omise hosted checkout not supported.'),
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Payment methods (Omise cards-on-customer) ────────────────────────
|
|
366
|
+
|
|
367
|
+
readonly paymentMethods: PaymentMethodOps = {
|
|
368
|
+
attach: async (paymentMethodId: string, customerId: string): Promise<PaymentMethod> => {
|
|
369
|
+
// Omise: pass the token id via `card`; the card joins the customer.
|
|
370
|
+
const updated = await this.client.customers.update(customerId, {
|
|
371
|
+
card: paymentMethodId,
|
|
372
|
+
})
|
|
373
|
+
const cards = (updated as { cards?: { data?: OmiseCard[] } }).cards
|
|
374
|
+
const card = cards?.data?.find((c) => c.id === paymentMethodId) ?? cards?.data?.[0]
|
|
375
|
+
if (!card) {
|
|
376
|
+
throw new ProviderUnsupportedError(
|
|
377
|
+
PROVIDER,
|
|
378
|
+
'paymentMethods.attach',
|
|
379
|
+
{ reason: 'Omise did not return the attached card on the customer payload.' },
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
return toPaymentMethod(card)
|
|
383
|
+
},
|
|
384
|
+
detach: async (paymentMethodId: string, customerId?: string): Promise<PaymentMethod> => {
|
|
385
|
+
if (!customerId) {
|
|
386
|
+
throw new ProviderUnsupportedError(
|
|
387
|
+
PROVIDER,
|
|
388
|
+
'paymentMethods.detach',
|
|
389
|
+
{ reason: 'Omise needs the owning customer id to detach a card. Call `paymentMethods.detach(cardId, customerId)`.' },
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
const card = await this.client.customers.destroyCard(customerId, paymentMethodId)
|
|
393
|
+
return toPaymentMethod(card)
|
|
394
|
+
},
|
|
395
|
+
list: async (
|
|
396
|
+
customerId: string,
|
|
397
|
+
options: ListPaymentMethodsOptions = {},
|
|
398
|
+
): Promise<PaginatedPaymentMethods> => {
|
|
399
|
+
const page = await this.client.customers.listCards(customerId, {
|
|
400
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
401
|
+
})
|
|
402
|
+
return {
|
|
403
|
+
data: page.data.map(toPaymentMethod),
|
|
404
|
+
nextCursor: null,
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ─── Charges ──────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
readonly charges: ChargeOps = {
|
|
412
|
+
create: async (input: CreateChargeInput): Promise<PaymentCharge> => {
|
|
413
|
+
const kind = paymentMethodKind(input.paymentMethod)
|
|
414
|
+
const cardToken = extractCardToken(input.paymentMethod)
|
|
415
|
+
const spec =
|
|
416
|
+
input.paymentMethod && typeof input.paymentMethod !== 'string'
|
|
417
|
+
? input.paymentMethod
|
|
418
|
+
: null
|
|
419
|
+
|
|
420
|
+
// Async two-step: build the source, then create the charge.
|
|
421
|
+
if (spec && spec.kind !== 'card') {
|
|
422
|
+
const build = buildOmiseMethodSpec(spec, input.amount, input.currency)
|
|
423
|
+
if (build.kind !== 'source') {
|
|
424
|
+
throw new ProviderUnsupportedError(
|
|
425
|
+
PROVIDER,
|
|
426
|
+
`charges.method.${kind}`,
|
|
427
|
+
{
|
|
428
|
+
reason: `Omise does not support payment-method kind "${kind}". Use the Stripe provider for paynow / kakaopay / konbini / fps, or call \`driver.client.sources.create\` directly for source types the framework hasn't bridged.`,
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
// Redirect-flow sources need a return_uri — Omise sends
|
|
433
|
+
// the customer back here after the wallet/redirect step.
|
|
434
|
+
const needsReturnUri = kind !== 'promptpay'
|
|
435
|
+
if (needsReturnUri && !input.returnUrl) {
|
|
436
|
+
throw new ProviderUnsupportedError(
|
|
437
|
+
PROVIDER,
|
|
438
|
+
`charges.method.${kind}`,
|
|
439
|
+
{
|
|
440
|
+
reason: `Omise requires a \`returnUrl\` for redirect-based payment methods (${kind}). Set \`config.payment.returnUrl\` or pass \`returnUrl\` on the call.`,
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
const source = await this.client.sources.create({
|
|
445
|
+
...build.request,
|
|
446
|
+
amount: input.amount,
|
|
447
|
+
currency: input.currency,
|
|
448
|
+
})
|
|
449
|
+
const c = await this.client.charges.create({
|
|
450
|
+
amount: input.amount,
|
|
451
|
+
currency: input.currency,
|
|
452
|
+
source: source.id,
|
|
453
|
+
...(input.customer ? { customer: input.customer } : {}),
|
|
454
|
+
...(input.returnUrl ? { return_uri: input.returnUrl } : {}),
|
|
455
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
456
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
457
|
+
})
|
|
458
|
+
// Re-attach the source we just created in case the charge
|
|
459
|
+
// payload didn't echo it back — the next-action mapper
|
|
460
|
+
// reads `scannable_code` off the source.
|
|
461
|
+
if (!c.source) c.source = source
|
|
462
|
+
return toPaymentCharge(c)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Single-step card path (today's behaviour).
|
|
466
|
+
const c = await this.client.charges.create({
|
|
467
|
+
amount: input.amount,
|
|
468
|
+
currency: input.currency,
|
|
469
|
+
...(input.customer ? { customer: input.customer } : {}),
|
|
470
|
+
...(cardToken ? { card: cardToken } : {}),
|
|
471
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
472
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
473
|
+
...(input.capture !== undefined ? { capture: input.capture } : {}),
|
|
474
|
+
})
|
|
475
|
+
return toPaymentCharge(c)
|
|
476
|
+
},
|
|
477
|
+
retrieve: async (id: string): Promise<PaymentCharge> => {
|
|
478
|
+
return toPaymentCharge(await this.client.charges.retrieve(id))
|
|
479
|
+
},
|
|
480
|
+
capture: async (id: string): Promise<PaymentCharge> => {
|
|
481
|
+
return toPaymentCharge(await this.client.charges.capture(id))
|
|
482
|
+
},
|
|
483
|
+
refund: async (input: CreateRefundInput): Promise<PaymentRefund> => {
|
|
484
|
+
const refund = await this.client.charges.createRefund(input.charge, {
|
|
485
|
+
...(input.amount !== undefined ? { amount: input.amount } : {}),
|
|
486
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
487
|
+
})
|
|
488
|
+
return {
|
|
489
|
+
id: refund.id,
|
|
490
|
+
provider: PROVIDER,
|
|
491
|
+
chargeId: refund.charge ?? input.charge,
|
|
492
|
+
amount: refund.amount,
|
|
493
|
+
currency: refund.currency.toLowerCase(),
|
|
494
|
+
status: refund.voided ? 'failed' : 'succeeded',
|
|
495
|
+
reason: input.reason ?? null,
|
|
496
|
+
createdAt: refund.created_at
|
|
497
|
+
? new Date(refund.created_at)
|
|
498
|
+
: refund.created
|
|
499
|
+
? new Date(refund.created)
|
|
500
|
+
: new Date(),
|
|
501
|
+
raw: refund,
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
readonly links: LinkOps = {
|
|
507
|
+
create: async (input: CreatePaymentLinkInput): Promise<PaymentLink> => {
|
|
508
|
+
if (input.items && input.items.length > 0) {
|
|
509
|
+
throw new ProviderUnsupportedError(
|
|
510
|
+
PROVIDER,
|
|
511
|
+
'links.create',
|
|
512
|
+
{
|
|
513
|
+
reason: 'Omise has no Prices catalogue. Pass `amount`, `currency`, `title`, and `description` directly instead of `items`.',
|
|
514
|
+
},
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
if (
|
|
518
|
+
input.amount === undefined ||
|
|
519
|
+
!input.currency ||
|
|
520
|
+
!input.title ||
|
|
521
|
+
!input.description
|
|
522
|
+
) {
|
|
523
|
+
throw new ProviderUnsupportedError(
|
|
524
|
+
PROVIDER,
|
|
525
|
+
'links.create',
|
|
526
|
+
{
|
|
527
|
+
reason: 'Omise links require `amount`, `currency`, `title`, and `description`. All four are mandatory.',
|
|
528
|
+
},
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
const link = await this.client.links.create({
|
|
532
|
+
amount: input.amount,
|
|
533
|
+
currency: input.currency,
|
|
534
|
+
title: input.title,
|
|
535
|
+
description: input.description,
|
|
536
|
+
...(input.reusable !== undefined ? { multiple: input.reusable } : {}),
|
|
537
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
538
|
+
})
|
|
539
|
+
return toPaymentLink(link)
|
|
540
|
+
},
|
|
541
|
+
retrieve: async (id: string): Promise<PaymentLink> => {
|
|
542
|
+
return toPaymentLink(await this.client.links.retrieve(id))
|
|
543
|
+
},
|
|
544
|
+
list: async (
|
|
545
|
+
options: ListPaymentLinksOptions = {},
|
|
546
|
+
): Promise<PaginatedPaymentLinks> => {
|
|
547
|
+
const page = await this.client.links.list({
|
|
548
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
549
|
+
})
|
|
550
|
+
const data = page.data.map(toPaymentLink)
|
|
551
|
+
const filtered =
|
|
552
|
+
options.active !== undefined
|
|
553
|
+
? data.filter((l) => l.active === options.active)
|
|
554
|
+
: data
|
|
555
|
+
return { data: filtered, nextCursor: null }
|
|
556
|
+
},
|
|
557
|
+
deactivate: async (_id: string): Promise<PaymentLink> => {
|
|
558
|
+
throw new ProviderUnsupportedError(
|
|
559
|
+
PROVIDER,
|
|
560
|
+
'links.deactivate',
|
|
561
|
+
{
|
|
562
|
+
reason: 'Omise has no link-deactivation endpoint. Single-use links (`reusable: false`) auto-expire after first payment; multi-use links remain active until manually deleted from the Omise Dashboard.',
|
|
563
|
+
},
|
|
564
|
+
)
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
readonly webhook: WebhookOps = {
|
|
569
|
+
verify: async (rawBody: string, signature: string): Promise<unknown> => {
|
|
570
|
+
return omiseVerify(rawBody, signature, this.config.webhookSecret)
|
|
571
|
+
},
|
|
572
|
+
normalize: (event: unknown): NormalizedWebhookEvent | null => {
|
|
573
|
+
return omiseNormalize(event as OmiseEvent)
|
|
574
|
+
},
|
|
575
|
+
}
|
|
576
|
+
}
|