medusa-payment-kadima 0.1.0

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.
@@ -0,0 +1,33 @@
1
+ // src/lib/webhook.ts
2
+ import { createHash } from "crypto";
3
+ function computeSignature(secret, envelope) {
4
+ const payload = `${secret}${envelope.id}${envelope.module}${envelope.action}${envelope.date}`;
5
+ return createHash("sha512").update(payload).digest("hex");
6
+ }
7
+ function verifySignature(secret, envelope, headerSignature) {
8
+ if (!headerSignature) return false;
9
+ const expected = computeSignature(secret, envelope);
10
+ if (expected.length !== headerSignature.length) return false;
11
+ let diff = 0;
12
+ for (let i = 0; i < expected.length; i++) {
13
+ diff |= expected.charCodeAt(i) ^ headerSignature.charCodeAt(i);
14
+ }
15
+ return diff === 0;
16
+ }
17
+
18
+ // src/types.ts
19
+ var CARD_HOSTS = {
20
+ prod: "https://gateway.kadimadashboard.com",
21
+ sandbox: "https://sandbox-gateway.kadimadashboard.com"
22
+ };
23
+ var DASHBOARD_HOSTS = {
24
+ prod: "https://kadimadashboard.com",
25
+ // CONFIRMED: dashboard/ACH sandbox host (distinct from the card gateway sandbox).
26
+ sandbox: "https://sandbox.kadimadashboard.com"
27
+ };
28
+
29
+ export {
30
+ CARD_HOSTS,
31
+ DASHBOARD_HOSTS,
32
+ verifySignature
33
+ };
@@ -0,0 +1,454 @@
1
+ import {
2
+ CARD_HOSTS,
3
+ DASHBOARD_HOSTS,
4
+ verifySignature
5
+ } from "./chunk-A4QO3PSZ.js";
6
+
7
+ // src/providers/kadima-card.ts
8
+ import { AbstractPaymentProvider, BigNumber } from "@medusajs/framework/utils";
9
+
10
+ // src/lib/errors.ts
11
+ var KadimaError = class extends Error {
12
+ constructor(message, status, reason, raw) {
13
+ super(message);
14
+ this.status = status;
15
+ this.reason = reason;
16
+ this.raw = raw;
17
+ this.name = "KadimaError";
18
+ }
19
+ };
20
+ var RETRYABLE = /* @__PURE__ */ new Set([
21
+ "Time out",
22
+ "System Error",
23
+ "Error on Host",
24
+ "Host connectivity failed",
25
+ "Issuer or Switch inoperative",
26
+ "Functionality currently not available"
27
+ ]);
28
+ function isRetryable(reason) {
29
+ return reason ? RETRYABLE.has(reason) : false;
30
+ }
31
+ function assertApproved(resp) {
32
+ const s = resp.status?.status;
33
+ if (s !== "Approved") {
34
+ throw new KadimaError(
35
+ `Kadima transaction ${s ?? "Error"}: ${resp.status?.reason ?? "unknown"}`,
36
+ s ?? "Error",
37
+ resp.status?.reason,
38
+ resp
39
+ );
40
+ }
41
+ }
42
+
43
+ // src/lib/kadima-card-client.ts
44
+ var KadimaCardClient = class {
45
+ constructor(opts) {
46
+ this.opts = opts;
47
+ this.gatewayBase = opts.sandbox ? CARD_HOSTS.sandbox : CARD_HOSTS.prod;
48
+ this.dashboardBase = opts.sandbox ? DASHBOARD_HOSTS.sandbox : DASHBOARD_HOSTS.prod;
49
+ }
50
+ // --- Hosted Fields: mint a single-use token for the storefront ----------
51
+ // Request: { expiration<=30, terminal, domain, saveCard, "3ds" }
52
+ async createHostedFieldsToken(input) {
53
+ return this.request(
54
+ `${this.dashboardBase}/api/hosted-fields/token`,
55
+ {
56
+ terminal: this.opts.terminalId,
57
+ domain: input.domain,
58
+ saveCard: input.saveCard ?? "disabled",
59
+ "3ds": input.threeds ?? false,
60
+ expiration: Math.min(input.expiration ?? 30, 30)
61
+ }
62
+ );
63
+ }
64
+ // Fetch the saved card token after a Hosted Fields payment (if saveCard allowed).
65
+ async getHostedFieldsCardToken(accessToken) {
66
+ return this.request(`${this.dashboardBase}/api/hosted-fields/card-token`, {
67
+ accessToken
68
+ });
69
+ }
70
+ // --- Server-to-server charges (stored token / recurring / MOTO) ----------
71
+ async auth(input) {
72
+ const resp = await this.request(
73
+ `${this.gatewayBase}/payment/auth`,
74
+ this.buildChargeBody(input)
75
+ );
76
+ assertApproved(resp);
77
+ return resp;
78
+ }
79
+ async sale(input) {
80
+ const resp = await this.request(
81
+ `${this.gatewayBase}/payment/sale`,
82
+ this.buildChargeBody(input)
83
+ );
84
+ assertApproved(resp);
85
+ return resp;
86
+ }
87
+ // POST /payment/<id>/capture body: { terminal, amount?, partial? }
88
+ async capture(id, amount, partial) {
89
+ const resp = await this.request(
90
+ `${this.gatewayBase}/payment/${id}/capture`,
91
+ {
92
+ terminal: { id: this.opts.terminalId },
93
+ ...amount != null ? { amount } : {},
94
+ ...partial ? { partial } : {}
95
+ }
96
+ );
97
+ assertApproved(resp);
98
+ return resp;
99
+ }
100
+ // POST /payment/<id>/refund body: { terminal, amount? }
101
+ async refund(id, amount) {
102
+ const resp = await this.request(
103
+ `${this.gatewayBase}/payment/${id}/refund`,
104
+ {
105
+ terminal: { id: this.opts.terminalId },
106
+ ...amount != null ? { amount } : {}
107
+ }
108
+ );
109
+ assertApproved(resp);
110
+ return resp;
111
+ }
112
+ /** Zero-dollar card validity check (no funds held). */
113
+ async cardAuthentication(input) {
114
+ return this.request(`${this.gatewayBase}/payment/card-authentication`, {
115
+ ...this.buildChargeBody(input),
116
+ amount: 0
117
+ });
118
+ }
119
+ /** Tokenize a card without an auth or sale. */
120
+ async generateToken(input) {
121
+ return this.request(`${this.gatewayBase}/payment/generate-token`, {
122
+ terminal: { id: this.opts.terminalId },
123
+ source: input.source ?? "Internet",
124
+ card: this.buildCard(input)
125
+ });
126
+ }
127
+ buildChargeBody(input) {
128
+ return {
129
+ terminal: { id: this.opts.terminalId },
130
+ amount: input.amount,
131
+ source: input.source ?? "Internet",
132
+ level: input.level ?? 1,
133
+ card: this.buildCard(input),
134
+ // externalId is the merchant-supplied correlation key (<=64 chars, unique).
135
+ ...input.externalId ? { externalId: input.externalId } : {},
136
+ ...input.contact ? { contact: input.contact } : {},
137
+ ...input.order ? { order: input.order } : {},
138
+ ...input.isRecurring ? { isRecurring: "Yes" } : {}
139
+ };
140
+ }
141
+ buildCard(input) {
142
+ if (input.cardToken) {
143
+ return {
144
+ token: input.cardToken,
145
+ ...input.store ? { store: "Yes" } : {},
146
+ ...input.networkTransactionId ? { networkTransactionId: input.networkTransactionId } : {}
147
+ };
148
+ }
149
+ return {
150
+ name: input.card?.name,
151
+ number: input.card?.number,
152
+ exp: input.card?.exp,
153
+ cvv: input.card?.cvv,
154
+ ...input.card?.address ? { address: input.card.address } : {},
155
+ ...input.save ? { save: "Yes" } : {},
156
+ ...input.store ? { store: "Yes" } : {}
157
+ };
158
+ }
159
+ // --- transport: bearer auth + retry on 5xx/network/retryable declines ----
160
+ async request(url, body, attempt = 0) {
161
+ let resp;
162
+ try {
163
+ resp = await fetch(url, {
164
+ method: "POST",
165
+ headers: {
166
+ Authorization: `Bearer ${this.opts.apiToken}`,
167
+ "Content-Type": "application/json"
168
+ },
169
+ body: JSON.stringify(body)
170
+ });
171
+ } catch (e) {
172
+ if (attempt < 2) return this.request(url, body, attempt + 1);
173
+ throw new KadimaError(`Network error calling Kadima: ${String(e)}`, "Error");
174
+ }
175
+ const text = await resp.text();
176
+ const json = text ? safeJson(text) : {};
177
+ if (resp.status >= 500 && attempt < 2) {
178
+ await delay(2 ** attempt * 500);
179
+ return this.request(url, body, attempt + 1);
180
+ }
181
+ if (!resp.ok) {
182
+ const msg = json?.message ?? text;
183
+ throw new KadimaError(`Kadima HTTP ${resp.status}: ${msg}`, "Error", void 0, json);
184
+ }
185
+ const reason = json?.status?.reason;
186
+ if (isRetryable(reason) && attempt < 2) {
187
+ await delay(2 ** attempt * 500);
188
+ return this.request(url, body, attempt + 1);
189
+ }
190
+ return json;
191
+ }
192
+ };
193
+ function safeJson(t) {
194
+ try {
195
+ return JSON.parse(t);
196
+ } catch {
197
+ return { raw: t };
198
+ }
199
+ }
200
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
201
+
202
+ // src/lib/kadima-vault-client.ts
203
+ var KadimaVaultClient = class {
204
+ constructor(opts) {
205
+ this.opts = opts;
206
+ const host = opts.sandbox ? DASHBOARD_HOSTS.sandbox : DASHBOARD_HOSTS.prod;
207
+ this.base = `${host}/api/customer-vault`;
208
+ }
209
+ // Customer requires dba.id. Returns the customer record incl. a vault `token`.
210
+ async createCustomer(input) {
211
+ if (this.opts.dbaId == null) {
212
+ throw new Error("KadimaVaultClient: dbaId is required for CustomerVault");
213
+ }
214
+ return this.request("POST", this.base, {
215
+ dba: { id: this.opts.dbaId },
216
+ ...input.firstName ? { firstName: input.firstName } : {},
217
+ ...input.lastName ? { lastName: input.lastName } : {},
218
+ ...input.company ? { company: input.company } : {},
219
+ ...input.email ? { email: input.email } : {},
220
+ ...input.phone ? { phone: input.phone } : {},
221
+ ...input.identificator ? { identificator: input.identificator } : {},
222
+ ...input.description ? { description: input.description } : {}
223
+ });
224
+ }
225
+ async getCustomer(id) {
226
+ return this.request("GET", `${this.base}/${id}`);
227
+ }
228
+ async updateCustomer(id, patch) {
229
+ return this.request("PUT", `${this.base}/${id}`, patch);
230
+ }
231
+ async deleteCustomer(id) {
232
+ return this.request("DELETE", `${this.base}/${id}`);
233
+ }
234
+ // Billing records — a card must reference a billing.id, so create one first.
235
+ async createBilling(customerId, billing) {
236
+ return this.request("POST", `${this.base}/${customerId}/billing-information`, billing);
237
+ }
238
+ async listBilling(customerId) {
239
+ return this.request("GET", `${this.base}/${customerId}/billing-information`);
240
+ }
241
+ async listCards(customerId) {
242
+ return this.request("GET", `${this.base}/${customerId}/cards`);
243
+ }
244
+ // Add a card with raw PAN (PCI-scoped path). exp is mm/yy.
245
+ async addCard(customerId, card) {
246
+ return this.request("POST", `${this.base}/${customerId}/card`, {
247
+ terminal: { id: this.opts.terminalId },
248
+ number: card.number,
249
+ exp: card.exp,
250
+ cvv: card.cvv,
251
+ holderName: card.holderName,
252
+ ...card.billingId != null ? { billing: { id: card.billingId } } : {}
253
+ });
254
+ }
255
+ async deleteCard(customerId, cardId) {
256
+ return this.request("DELETE", `${this.base}/${customerId}/card/${cardId}`);
257
+ }
258
+ async request(method, url, body) {
259
+ const resp = await fetch(url, {
260
+ method,
261
+ headers: {
262
+ Authorization: `Bearer ${this.opts.apiToken}`,
263
+ "Content-Type": "application/json"
264
+ },
265
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
266
+ });
267
+ const text = await resp.text();
268
+ const json = text ? JSON.parse(text) : {};
269
+ if (!resp.ok) {
270
+ const msg = json?.message ?? text;
271
+ throw new Error(`Kadima Vault HTTP ${resp.status}: ${msg}`);
272
+ }
273
+ return json;
274
+ }
275
+ };
276
+
277
+ // src/providers/kadima-card.ts
278
+ var KadimaCardProviderService = class extends AbstractPaymentProvider {
279
+ static {
280
+ this.identifier = "kadima-card";
281
+ }
282
+ constructor(container, options) {
283
+ super(container, options);
284
+ this.options_ = options;
285
+ this.client = new KadimaCardClient(options);
286
+ this.vault = new KadimaVaultClient(options);
287
+ }
288
+ /**
289
+ * No money moves. Mint a Hosted Fields token so the storefront can collect the
290
+ * card client-side. The returned `data` is public — only the HF token + terminal.
291
+ */
292
+ async initiatePayment(input) {
293
+ const domain = input.data?.domain ?? "";
294
+ const hf = await this.client.createHostedFieldsToken({
295
+ domain,
296
+ saveCard: input.context?.account_holder ? "optional" : "disabled"
297
+ });
298
+ return {
299
+ // No Kadima transaction exists yet; use a local placeholder id.
300
+ id: `kadima_hf_${hf.access_token.slice(-12)}`,
301
+ data: {
302
+ hostedFieldsToken: hf.access_token,
303
+ terminalId: this.options_.terminalId,
304
+ amount: input.amount,
305
+ currency_code: input.currency_code
306
+ }
307
+ };
308
+ }
309
+ /**
310
+ * Two paths (see docs/storefront/hosted-fields-example.md):
311
+ *
312
+ * A) Hosted Fields (new card): the BROWSER already performed the payment. No
313
+ * card token reaches us. We return `pending` and let the `transaction/create`
314
+ * webhook (keyed by externalId === session_id) authorize the session.
315
+ *
316
+ * B) Server-to-server (stored CustomerVault token / recurring / MOTO): we hold
317
+ * a reusable card token and charge directly. captureMethod=auth → /payment/auth;
318
+ * =sale → /payment/sale.
319
+ */
320
+ async authorizePayment(input) {
321
+ const cardToken = input.data?.cardToken;
322
+ const sessionId = input.data?.session_id;
323
+ if (!cardToken) {
324
+ return { status: "pending", data: { ...input.data, externalId: sessionId } };
325
+ }
326
+ const amount = Number(input.data?.amount);
327
+ const method = this.options_.captureMethod ?? "auth";
328
+ const txn = method === "sale" ? await this.client.sale({ amount, cardToken, externalId: sessionId }) : await this.client.auth({ amount, cardToken, externalId: sessionId });
329
+ return {
330
+ status: method === "sale" ? "captured" : "authorized",
331
+ data: { id: txn.id, captured: txn.captured, raw: txn }
332
+ };
333
+ }
334
+ async capturePayment(input) {
335
+ const id = String(input.data?.id);
336
+ if (input.data?.captured) return { data: input.data };
337
+ const txn = await this.client.capture(id);
338
+ return { data: { ...input.data, captured: txn.captured, raw: txn } };
339
+ }
340
+ async refundPayment(input) {
341
+ const id = String(input.data?.id);
342
+ const txn = await this.client.refund(id, Number(input.amount));
343
+ return { data: { ...input.data, refundId: txn.id, raw: txn } };
344
+ }
345
+ async cancelPayment(input) {
346
+ const id = String(input.data?.id);
347
+ const txn = await this.client.refund(id);
348
+ return { data: { ...input.data, canceled: true, raw: txn } };
349
+ }
350
+ async getPaymentStatus(input) {
351
+ const d = input.data ?? {};
352
+ if (d.refundId) return { status: "captured", data: d };
353
+ if (d.captured) return { status: "captured", data: d };
354
+ if (d.id) return { status: "authorized", data: d };
355
+ return { status: "pending", data: d };
356
+ }
357
+ async retrievePayment(input) {
358
+ return { data: input.data ?? {} };
359
+ }
360
+ async updatePayment(input) {
361
+ return { data: { ...input.data, amount: input.amount } };
362
+ }
363
+ async deletePayment(input) {
364
+ return { data: input.data ?? {} };
365
+ }
366
+ // --- Account holders & saved cards (CustomerVault) -----------------------
367
+ /** Create a CustomerVault customer for this Medusa customer. */
368
+ async createAccountHolder(input) {
369
+ const c = input.context.customer;
370
+ const customer = await this.vault.createCustomer({
371
+ firstName: c.first_name ?? void 0,
372
+ lastName: c.last_name ?? void 0,
373
+ company: c.company_name ?? void 0,
374
+ email: c.email,
375
+ phone: c.phone ?? void 0,
376
+ identificator: c.id
377
+ // Medusa customer id
378
+ });
379
+ return { id: String(customer.id), data: { ...customer } };
380
+ }
381
+ async deleteAccountHolder(input) {
382
+ const id = input.context.account_holder?.data?.id;
383
+ if (id) await this.vault.deleteCustomer(id);
384
+ return {};
385
+ }
386
+ async listPaymentMethods(input) {
387
+ const customerId = input.context?.account_holder?.data?.id;
388
+ if (!customerId) return [];
389
+ const { items } = await this.vault.listCards(customerId);
390
+ return (items ?? []).map((card) => ({
391
+ // The card token is the id we charge with later (card.token).
392
+ id: card.token,
393
+ data: { ...card }
394
+ }));
395
+ }
396
+ /**
397
+ * Save a card to the vault. Two inputs are supported in `data`:
398
+ * - raw card { number, exp, cvv, holderName } → POST .../card (PCI-scoped)
399
+ * - an already-tokenized card { token } → stored as-is (from Hosted
400
+ * Fields card-token, since there is no attach-token-to-vault endpoint)
401
+ */
402
+ async savePaymentMethod(input) {
403
+ const data = input.data ?? {};
404
+ const customerId = input.context?.account_holder?.data?.id;
405
+ if (data.token && !data.number) {
406
+ return { id: data.token, data };
407
+ }
408
+ if (!customerId) {
409
+ throw new Error("savePaymentMethod requires an account holder (vault customer)");
410
+ }
411
+ const card = await this.vault.addCard(customerId, {
412
+ number: data.number,
413
+ exp: data.exp,
414
+ cvv: data.cvv,
415
+ holderName: data.holderName
416
+ });
417
+ return { id: card.token, data: { ...card } };
418
+ }
419
+ async getWebhookActionAndData(payload) {
420
+ const data = payload.data;
421
+ const headers = payload.headers;
422
+ const ok = verifySignature(
423
+ this.options_.webhookSecret,
424
+ {
425
+ id: data.id,
426
+ module: data.module,
427
+ action: data.action,
428
+ date: data.date
429
+ },
430
+ headers["webhook-signature"] || headers["Webhook-Signature"]
431
+ );
432
+ if (!ok || data.module !== "transaction") {
433
+ return { action: "not_supported" };
434
+ }
435
+ const txn = data.data ?? {};
436
+ const session_id = txn.externalId ?? "";
437
+ const amount = new BigNumber(Number(txn.amount ?? 0));
438
+ switch (data.action) {
439
+ case "create": {
440
+ const action = txn.captured ? "captured" : "authorized";
441
+ return { action, data: { session_id, amount } };
442
+ }
443
+ case "refund":
444
+ return { action: "captured", data: { session_id, amount } };
445
+ default:
446
+ return { action: "not_supported" };
447
+ }
448
+ }
449
+ };
450
+ var kadima_card_default = KadimaCardProviderService;
451
+
452
+ export {
453
+ kadima_card_default
454
+ };
@@ -0,0 +1,197 @@
1
+ import {
2
+ DASHBOARD_HOSTS,
3
+ verifySignature
4
+ } from "./chunk-A4QO3PSZ.js";
5
+
6
+ // src/providers/kadima-ach.ts
7
+ import { AbstractPaymentProvider } from "@medusajs/framework/utils";
8
+ import { BigNumber } from "@medusajs/framework/utils";
9
+
10
+ // src/lib/kadima-ach-client.ts
11
+ var KadimaAchClient = class {
12
+ constructor(opts) {
13
+ this.opts = opts;
14
+ this.base = `${opts.sandbox ? DASHBOARD_HOSTS.sandbox : DASHBOARD_HOSTS.prod}/api/ach`;
15
+ }
16
+ // Create returns { id }. NOTE: id is a string in the minimal response but
17
+ // numeric in the save-customer response — treat it as string | number.
18
+ /** Create a debit. Returns the new ACH transaction id. */
19
+ async debit(input) {
20
+ return this.request(this.base, "POST", this.buildBody(input, "Debit"));
21
+ }
22
+ /** Offsetting credit — refunds a settled debit (PPD/CCD only). */
23
+ async credit(input) {
24
+ return this.request(this.base, "POST", this.buildBody(input, "Credit"));
25
+ }
26
+ /** void | cancel | verify (void only while Held/Pending/Submitted). */
27
+ async action(id, action) {
28
+ return this.request(`${this.base}/${id}/${action}`, "POST");
29
+ }
30
+ async get(id) {
31
+ return this.request(`${this.base}/${id}`, "GET");
32
+ }
33
+ // --- Customer Vault ------------------------------------------------------
34
+ async createCustomer(data) {
35
+ return this.request(`${this.base}/customer`, "POST", data);
36
+ }
37
+ async addAccount(customerId, account) {
38
+ return this.request(`${this.base}/customer/${customerId}/account`, "POST", account);
39
+ }
40
+ buildBody(input, transactionType) {
41
+ const body = {
42
+ amount: input.amount,
43
+ transactionType,
44
+ SECCode: input.secCode ?? this.opts.secCode ?? "WEB",
45
+ dba: { id: this.opts.dbaId },
46
+ ...input.tax != null ? { tax: input.tax } : {},
47
+ ...input.memo ? { memo: input.memo } : {},
48
+ ...input.addendaText ? { addendaText: input.addendaText } : {}
49
+ };
50
+ if (input.customerId) {
51
+ body.customer = { id: input.customerId };
52
+ if (input.accountId) body.account = { id: input.accountId };
53
+ } else {
54
+ body.accountName = input.accountName;
55
+ body.accountNumber = input.accountNumber;
56
+ body.routingNumber = input.routingNumber;
57
+ body.accountType = input.accountType ?? "Checking";
58
+ body.customer = {
59
+ ...input.saveCustomer ? { save: "Yes" } : {},
60
+ ...input.customer
61
+ };
62
+ }
63
+ return body;
64
+ }
65
+ async request(url, method, body) {
66
+ const resp = await fetch(url, {
67
+ method,
68
+ headers: {
69
+ Authorization: `Bearer ${this.opts.apiToken}`,
70
+ "Content-Type": "application/json"
71
+ },
72
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
73
+ });
74
+ const text = await resp.text();
75
+ const json = text ? JSON.parse(text) : {};
76
+ if (!resp.ok) {
77
+ const msg = json?.message ?? text;
78
+ throw new Error(`Kadima ACH HTTP ${resp.status}: ${msg}`);
79
+ }
80
+ return json;
81
+ }
82
+ };
83
+
84
+ // src/providers/kadima-ach.ts
85
+ var KadimaAchProviderService = class extends AbstractPaymentProvider {
86
+ static {
87
+ this.identifier = "kadima-ach";
88
+ }
89
+ constructor(container, options) {
90
+ super(container, options);
91
+ this.options_ = options;
92
+ this.client = new KadimaAchClient(options);
93
+ }
94
+ async initiatePayment(input) {
95
+ return {
96
+ id: `kadima_ach_init`,
97
+ data: { amount: input.amount, currency_code: input.currency_code }
98
+ };
99
+ }
100
+ /**
101
+ * Submit the debit. The item is created `Pending` → we return `authorized`
102
+ * (funds are NOT settled yet). The `Settled` webhook later drives `captured`.
103
+ *
104
+ * Correlation: we stamp `customer.identifier = session_id` so the achObject on
105
+ * the webhook carries our Medusa session id back to us.
106
+ */
107
+ async authorizePayment(input) {
108
+ const sessionId = input.data?.session_id;
109
+ const inlineCustomer = input.data?.customer ?? {};
110
+ const created = await this.client.debit({
111
+ amount: Number(input.data?.amount),
112
+ tax: input.data?.tax != null ? Number(input.data.tax) : void 0,
113
+ customerId: input.data?.customerId,
114
+ accountId: input.data?.accountId,
115
+ // inline bank account (top-level fields) for a first-time payer
116
+ accountName: input.data?.accountName,
117
+ accountNumber: input.data?.accountNumber,
118
+ routingNumber: input.data?.routingNumber,
119
+ accountType: input.data?.accountType,
120
+ saveCustomer: Boolean(input.data?.saveCustomer),
121
+ customer: { ...inlineCustomer, identifier: sessionId }
122
+ });
123
+ return {
124
+ status: "authorized",
125
+ data: { id: created.id, achStatus: "Pending" }
126
+ };
127
+ }
128
+ /** No synchronous capture for ACH — real capture is the Settled webhook. */
129
+ async capturePayment(input) {
130
+ return { data: input.data ?? {} };
131
+ }
132
+ async refundPayment(input) {
133
+ const id = String(input.data?.id);
134
+ const status = input.data?.achStatus;
135
+ if (status === "Pending" || status === "Submitted") {
136
+ await this.client.action(id, "void");
137
+ return { data: { ...input.data, voided: true } };
138
+ }
139
+ const credit = await this.client.credit({
140
+ amount: Number(input.amount),
141
+ customerId: input.data?.customerId
142
+ });
143
+ return { data: { ...input.data, creditId: credit.id } };
144
+ }
145
+ async cancelPayment(input) {
146
+ const id = String(input.data?.id);
147
+ await this.client.action(id, "void");
148
+ return { data: { ...input.data, canceled: true } };
149
+ }
150
+ async getPaymentStatus(input) {
151
+ const s = input.data?.achStatus;
152
+ switch (s) {
153
+ case "Settled":
154
+ return { status: "captured", data: input.data ?? {} };
155
+ case "Returned":
156
+ case "Voided":
157
+ return { status: "canceled", data: input.data ?? {} };
158
+ default:
159
+ return { status: "authorized", data: input.data ?? {} };
160
+ }
161
+ }
162
+ async retrievePayment(input) {
163
+ return { data: input.data ?? {} };
164
+ }
165
+ async updatePayment(input) {
166
+ return { data: { ...input.data, amount: input.amount } };
167
+ }
168
+ async deletePayment(input) {
169
+ return { data: input.data ?? {} };
170
+ }
171
+ async getWebhookActionAndData(payload) {
172
+ const data = payload.data;
173
+ const headers = payload.headers;
174
+ const ok = verifySignature(
175
+ this.options_.webhookSecret,
176
+ { id: data.id, module: data.module, action: data.action, date: data.date },
177
+ headers["webhook-signature"] || headers["Webhook-Signature"]
178
+ );
179
+ if (!ok || data.module !== "ach") return { action: "not_supported" };
180
+ const ach = data.data ?? {};
181
+ const sessionId = ach.customer?.identifier ?? "";
182
+ const amount = new BigNumber(Number(ach.amount ?? 0));
183
+ const status = ach.transactionStatus;
184
+ if (data.action === "updateStatus" && status === "Settled") {
185
+ return { action: "captured", data: { session_id: sessionId, amount } };
186
+ }
187
+ if (data.action === "updateStatus" && (status === "Returned" || status === "Voided")) {
188
+ return { action: "failed", data: { session_id: sessionId, amount } };
189
+ }
190
+ return { action: "not_supported" };
191
+ }
192
+ };
193
+ var kadima_ach_default = KadimaAchProviderService;
194
+
195
+ export {
196
+ kadima_ach_default
197
+ };