medusa-payment-kadima 0.1.0 → 0.1.2

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.
@@ -2,7 +2,7 @@ import {
2
2
  CARD_HOSTS,
3
3
  DASHBOARD_HOSTS,
4
4
  verifySignature
5
- } from "./chunk-A4QO3PSZ.js";
5
+ } from "./chunk-RPREKUY6.js";
6
6
 
7
7
  // src/providers/kadima-card.ts
8
8
  import { AbstractPaymentProvider, BigNumber } from "@medusajs/framework/utils";
@@ -284,6 +284,10 @@ var KadimaCardProviderService = class extends AbstractPaymentProvider {
284
284
  this.options_ = options;
285
285
  this.client = new KadimaCardClient(options);
286
286
  this.vault = new KadimaVaultClient(options);
287
+ const sb = !!options.sandbox;
288
+ console.info(
289
+ `[kadima-card] init \u2014 sandbox=${sb} \xB7 gateway=${sb ? CARD_HOSTS.sandbox : CARD_HOSTS.prod} \xB7 dashboard=${sb ? DASHBOARD_HOSTS.sandbox : DASHBOARD_HOSTS.prod} \xB7 terminal=${options.terminalId ?? "(unset)"} \xB7 token=${options.apiToken ? "set" : "MISSING"}`
290
+ );
287
291
  }
288
292
  /**
289
293
  * No money moves. Mint a Hosted Fields token so the storefront can collect the
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DASHBOARD_HOSTS,
3
3
  verifySignature
4
- } from "./chunk-A4QO3PSZ.js";
4
+ } from "./chunk-RPREKUY6.js";
5
5
 
6
6
  // src/providers/kadima-ach.ts
7
7
  import { AbstractPaymentProvider } from "@medusajs/framework/utils";
@@ -90,6 +90,10 @@ var KadimaAchProviderService = class extends AbstractPaymentProvider {
90
90
  super(container, options);
91
91
  this.options_ = options;
92
92
  this.client = new KadimaAchClient(options);
93
+ const sb = !!options.sandbox;
94
+ console.info(
95
+ `[kadima-ach] init \u2014 sandbox=${sb} \xB7 host=${sb ? DASHBOARD_HOSTS.sandbox : DASHBOARD_HOSTS.prod}/api/ach \xB7 dba=${options.dbaId ?? "(unset)"} \xB7 token=${options.apiToken ? "set" : "MISSING"}`
96
+ );
93
97
  }
94
98
  async initiatePayment(input) {
95
99
  return {
@@ -21,7 +21,9 @@ import './types-BUuMxNOL.js';
21
21
  * terminalId: Number(process.env.KADIMA_TERMINAL_ID),
22
22
  * webhookSecret: process.env.KADIMA_WEBHOOK_SECRET,
23
23
  * captureMethod: "auth", // or "sale"
24
- * sandbox: process.env.NODE_ENV !== "production",
24
+ * // Set explicitly — NODE_ENV is "production" on most hosts, which
25
+ * // would silently send sandbox creds to the LIVE host (→ 401).
26
+ * sandbox: process.env.KADIMA_SANDBOX === "true",
25
27
  * },
26
28
  * },
27
29
  * {
@@ -32,6 +34,7 @@ import './types-BUuMxNOL.js';
32
34
  * dbaId: Number(process.env.KADIMA_DBA_ID),
33
35
  * webhookSecret: process.env.KADIMA_WEBHOOK_SECRET,
34
36
  * secCode: "WEB",
37
+ * sandbox: process.env.KADIMA_SANDBOX === "true",
35
38
  * },
36
39
  * },
37
40
  * ],
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  kadima_ach_default
3
- } from "./chunk-R62JQB26.js";
3
+ } from "./chunk-UO5EBYD2.js";
4
4
  import {
5
5
  kadima_card_default
6
- } from "./chunk-MKSGS2DZ.js";
7
- import "./chunk-A4QO3PSZ.js";
6
+ } from "./chunk-KVJTOLRY.js";
7
+ import "./chunk-RPREKUY6.js";
8
8
 
9
9
  // src/index.ts
10
10
  import { ModuleProvider, Modules } from "@medusajs/framework/utils";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  kadima_ach_default
3
- } from "../chunk-R62JQB26.js";
4
- import "../chunk-A4QO3PSZ.js";
3
+ } from "../chunk-UO5EBYD2.js";
4
+ import "../chunk-RPREKUY6.js";
5
5
  export {
6
6
  kadima_ach_default as default
7
7
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  kadima_card_default
3
- } from "../chunk-MKSGS2DZ.js";
4
- import "../chunk-A4QO3PSZ.js";
3
+ } from "../chunk-KVJTOLRY.js";
4
+ import "../chunk-RPREKUY6.js";
5
5
  export {
6
6
  kadima_card_default as default
7
7
  };
package/README.md CHANGED
@@ -38,7 +38,7 @@ module.exports = defineConfig({
38
38
  dbaId: Number(process.env.KADIMA_DBA_ID), // for saved cards
39
39
  webhookSecret: process.env.KADIMA_WEBHOOK_SECRET,
40
40
  captureMethod: "auth", // or "sale"
41
- sandbox: process.env.NODE_ENV !== "production",
41
+ sandbox: process.env.KADIMA_SANDBOX === "true",
42
42
  },
43
43
  },
44
44
  {
@@ -49,7 +49,7 @@ module.exports = defineConfig({
49
49
  dbaId: Number(process.env.KADIMA_DBA_ID),
50
50
  webhookSecret: process.env.KADIMA_WEBHOOK_SECRET,
51
51
  secCode: "WEB",
52
- sandbox: process.env.NODE_ENV !== "production",
52
+ sandbox: process.env.KADIMA_SANDBOX === "true",
53
53
  },
54
54
  },
55
55
  ],
@@ -59,6 +59,24 @@ module.exports = defineConfig({
59
59
  })
60
60
  ```
61
61
 
62
+ > ⚠️ **Set `sandbox` explicitly — do not derive it from `NODE_ENV`.** Most hosting
63
+ > platforms (Railway, Render, Vercel, Heroku, Fly) set `NODE_ENV=production`, so
64
+ > `NODE_ENV !== "production"` silently evaluates to `false` and your test build hits
65
+ > the **live** Kadima hosts with sandbox credentials → `HTTP 401: invalid credentials`.
66
+ > While testing, set `KADIMA_SANDBOX=true` in your env; unset it (or `false`) for production.
67
+ > `sandbox: true` uses `sandbox.kadimadashboard.com` / `sandbox-gateway.kadimadashboard.com`;
68
+ > `false` uses the live hosts. The token, terminal ID and DBA ID must come from the
69
+ > **same** environment as the flag (a sandbox token on a live host, or vice-versa, is a 401).
70
+
71
+ On startup each provider logs its resolved configuration so mismatches are obvious, e.g.:
72
+
73
+ ```
74
+ [kadima-card] init — sandbox=true · gateway=https://sandbox-gateway.kadimadashboard.com · dashboard=https://sandbox.kadimadashboard.com · terminal=404 · token=set
75
+ ```
76
+
77
+ If you see `token=MISSING`, the env var isn't set in your deployment. If `sandbox` or the
78
+ host is wrong for your token, fix the flag and redeploy.
79
+
62
80
  Enable the providers in your sales channel / region, point your Kadima webhook at
63
81
  `https://<your-store>/hooks/payment/kadima-card` (and `/kadima-ach`), and add the
64
82
  storefront components from [`storefront/`](./storefront/README.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "medusa-payment-kadima",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Medusa v2 payment provider for Kadima — direct card + ACH, no middle gateway",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -26,20 +26,22 @@
26
26
  "exports": {
27
27
  ".": {
28
28
  "types": "./.medusa/server/src/index.d.ts",
29
- "import": "./.medusa/server/src/index.js"
29
+ "import": "./.medusa/server/src/index.js",
30
+ "default": "./.medusa/server/src/index.js"
30
31
  },
31
32
  "./providers/kadima-card": {
32
33
  "types": "./.medusa/server/src/providers/kadima-card.d.ts",
33
- "import": "./.medusa/server/src/providers/kadima-card.js"
34
+ "import": "./.medusa/server/src/providers/kadima-card.js",
35
+ "default": "./.medusa/server/src/providers/kadima-card.js"
34
36
  },
35
37
  "./providers/kadima-ach": {
36
38
  "types": "./.medusa/server/src/providers/kadima-ach.d.ts",
37
- "import": "./.medusa/server/src/providers/kadima-ach.js"
39
+ "import": "./.medusa/server/src/providers/kadima-ach.js",
40
+ "default": "./.medusa/server/src/providers/kadima-ach.js"
38
41
  }
39
42
  },
40
43
  "files": [
41
44
  ".medusa/server",
42
- "src",
43
45
  "storefront",
44
46
  "README.md"
45
47
  ],
package/src/index.ts DELETED
@@ -1,43 +0,0 @@
1
- import { ModuleProvider, Modules } from "@medusajs/framework/utils"
2
- import KadimaCardProviderService from "./providers/kadima-card"
3
- import KadimaAchProviderService from "./providers/kadima-ach"
4
-
5
- /**
6
- * Registers BOTH providers under the Payment module. In medusa-config.ts:
7
- *
8
- * modules: [
9
- * {
10
- * resolve: "@medusajs/medusa/payment",
11
- * options: {
12
- * providers: [
13
- * {
14
- * resolve: "medusa-payment-kadima/providers/kadima-card",
15
- * id: "kadima-card",
16
- * options: {
17
- * apiToken: process.env.KADIMA_CARD_TOKEN,
18
- * terminalId: Number(process.env.KADIMA_TERMINAL_ID),
19
- * webhookSecret: process.env.KADIMA_WEBHOOK_SECRET,
20
- * captureMethod: "auth", // or "sale"
21
- * sandbox: process.env.NODE_ENV !== "production",
22
- * },
23
- * },
24
- * {
25
- * resolve: "medusa-payment-kadima/providers/kadima-ach",
26
- * id: "kadima-ach",
27
- * options: {
28
- * apiToken: process.env.KADIMA_ACH_TOKEN,
29
- * dbaId: Number(process.env.KADIMA_DBA_ID),
30
- * webhookSecret: process.env.KADIMA_WEBHOOK_SECRET,
31
- * secCode: "WEB",
32
- * },
33
- * },
34
- * ],
35
- * },
36
- * },
37
- * ]
38
- */
39
- export default ModuleProvider(Modules.PAYMENT, {
40
- services: [KadimaCardProviderService, KadimaAchProviderService],
41
- })
42
-
43
- export { KadimaCardProviderService, KadimaAchProviderService }
package/src/lib/errors.ts DELETED
@@ -1,46 +0,0 @@
1
- import { KadimaTxnStatus } from "../types"
2
-
3
- /**
4
- * Kadima returns a rich set of decline reasons (see docs/2-Payment Gateway API.pdf).
5
- * We surface the raw reason but classify a few that Medusa / the storefront should
6
- * treat specially (retryable vs hard decline vs needs-action).
7
- */
8
- export class KadimaError extends Error {
9
- constructor(
10
- message: string,
11
- readonly status: KadimaTxnStatus,
12
- readonly reason?: string | null,
13
- readonly raw?: unknown
14
- ) {
15
- super(message)
16
- this.name = "KadimaError"
17
- }
18
- }
19
-
20
- const RETRYABLE = 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
-
29
- export function isRetryable(reason?: string): boolean {
30
- return reason ? RETRYABLE.has(reason) : false
31
- }
32
-
33
- /** Throw if Kadima did not approve. */
34
- export function assertApproved(resp: {
35
- status?: { status?: KadimaTxnStatus; reason?: string | null }
36
- }): void {
37
- const s = resp.status?.status
38
- if (s !== "Approved") {
39
- throw new KadimaError(
40
- `Kadima transaction ${s ?? "Error"}: ${resp.status?.reason ?? "unknown"}`,
41
- s ?? "Error",
42
- resp.status?.reason,
43
- resp
44
- )
45
- }
46
- }
@@ -1,166 +0,0 @@
1
- import { DASHBOARD_HOSTS, KadimaAchOptions } from "../types"
2
-
3
- /**
4
- * Client over the Kadima ACH API (kadimadashboard.com/api/ach).
5
- *
6
- * Verified against Kadima_Dashboard_API_COMPLETE.md:
7
- * POST /api/ach create a Debit/Credit (returns { id })
8
- * GET /api/ach/<id> view a transaction
9
- * POST /api/ach/<id>/<action> void | cancel | verify
10
- * POST /api/ach/customer create a vault customer
11
- * POST /api/ach/customer/<id>/account add a bank account to a customer
12
- *
13
- * ACH is ASYNCHRONOUS: create returns status Pending; settlement (`Settled`) or
14
- * failure (`Returned`) arrive later via the ach/updateStatus webhook. So a debit
15
- * create means "submitted", not "captured".
16
- *
17
- * Request field names are exact and case-sensitive: `SECCode`, `transactionType`,
18
- * `accountName`, `accountNumber`, `routingNumber`, `accountType`, `dba.id`.
19
- */
20
- export class KadimaAchClient {
21
- private readonly base: string
22
-
23
- constructor(private readonly opts: KadimaAchOptions) {
24
- this.base = `${opts.sandbox ? DASHBOARD_HOSTS.sandbox : DASHBOARD_HOSTS.prod}/api/ach`
25
- }
26
-
27
- // Create returns { id }. NOTE: id is a string in the minimal response but
28
- // numeric in the save-customer response — treat it as string | number.
29
- /** Create a debit. Returns the new ACH transaction id. */
30
- async debit(input: AchTxnInput): Promise<{ id: string | number }> {
31
- return this.request(this.base, "POST", this.buildBody(input, "Debit"))
32
- }
33
-
34
- /** Offsetting credit — refunds a settled debit (PPD/CCD only). */
35
- async credit(input: AchTxnInput): Promise<{ id: string | number }> {
36
- return this.request(this.base, "POST", this.buildBody(input, "Credit"))
37
- }
38
-
39
- /** void | cancel | verify (void only while Held/Pending/Submitted). */
40
- async action(id: string | number, action: "void" | "cancel" | "verify") {
41
- return this.request(`${this.base}/${id}/${action}`, "POST")
42
- }
43
-
44
- async get(id: string | number): Promise<KadimaAchTxn> {
45
- return this.request(`${this.base}/${id}`, "GET")
46
- }
47
-
48
- // --- Customer Vault ------------------------------------------------------
49
- async createCustomer(data: Record<string, unknown>): Promise<{ id: string }> {
50
- return this.request(`${this.base}/customer`, "POST", data)
51
- }
52
- async addAccount(customerId: string | number, account: Record<string, unknown>) {
53
- return this.request(`${this.base}/customer/${customerId}/account`, "POST", account)
54
- }
55
-
56
- private buildBody(input: AchTxnInput, transactionType: "Debit" | "Credit") {
57
- const body: Record<string, unknown> = {
58
- amount: input.amount,
59
- transactionType,
60
- SECCode: input.secCode ?? this.opts.secCode ?? "WEB",
61
- dba: { id: this.opts.dbaId },
62
- ...(input.tax != null ? { tax: input.tax } : {}),
63
- ...(input.memo ? { memo: input.memo } : {}),
64
- ...(input.addendaText ? { addendaText: input.addendaText } : {}),
65
- }
66
-
67
- if (input.customerId) {
68
- // Existing vault customer (+ optional specific saved account).
69
- body.customer = { id: input.customerId }
70
- if (input.accountId) body.account = { id: input.accountId }
71
- } else {
72
- // New inline bank account. Bank fields are TOP-LEVEL, not nested.
73
- body.accountName = input.accountName
74
- body.accountNumber = input.accountNumber
75
- body.routingNumber = input.routingNumber
76
- body.accountType = input.accountType ?? "Checking"
77
- body.customer = {
78
- ...(input.saveCustomer ? { save: "Yes" } : {}),
79
- ...input.customer,
80
- }
81
- }
82
- return body
83
- }
84
-
85
- private async request<T = any>(
86
- url: string,
87
- method: "POST" | "GET",
88
- body?: unknown
89
- ): Promise<T> {
90
- const resp = await fetch(url, {
91
- method,
92
- headers: {
93
- Authorization: `Bearer ${this.opts.apiToken}`,
94
- "Content-Type": "application/json",
95
- },
96
- ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
97
- })
98
- const text = await resp.text()
99
- const json = text ? JSON.parse(text) : {}
100
- if (!resp.ok) {
101
- const msg = (json as any)?.message ?? text
102
- throw new Error(`Kadima ACH HTTP ${resp.status}: ${msg}`)
103
- }
104
- return json as T
105
- }
106
- }
107
-
108
- export interface AchTxnInput {
109
- amount: number
110
- tax?: number
111
- secCode?: "PPD" | "CCD" | "WEB" | "TEL"
112
- memo?: string
113
- addendaText?: string
114
- // Either an existing vault customer ...
115
- customerId?: string | number
116
- accountId?: string | number
117
- // ... or a new inline bank account (top-level fields) + customer details.
118
- accountName?: string
119
- accountNumber?: string
120
- routingNumber?: string
121
- accountType?: "Checking" | "Savings"
122
- saveCustomer?: boolean // customer.save = "Yes"
123
- customer?: {
124
- firstName?: string
125
- lastName?: string
126
- email?: string
127
- phone?: string
128
- address1?: string
129
- address2?: string
130
- city?: string
131
- state?: string
132
- zipCode?: string
133
- identifier?: string // merchant-side unique id for the payer
134
- sendReceipt?: "Yes" | "No"
135
- }
136
- }
137
-
138
- /** ACH transaction object (List/View/webhook achObject). */
139
- export interface KadimaAchTxn {
140
- id: number | string
141
- amount: string
142
- tax?: string
143
- SECCode?: "PPD" | "CCD" | "WEB" | "TEL"
144
- accountName?: string
145
- accountNumber?: string
146
- accountType?: "Checking" | "Savings"
147
- routingNumber?: string
148
- transactionType?: "Debit" | "Credit"
149
- transactionStatus?: KadimaAchStatus
150
- dba?: { id: number; name?: string }
151
- customer?: { id?: string | null; identifier?: string | null; email?: string | null }
152
- verification?: { status?: string; date?: string | null }
153
- return?: { code?: string | null; date?: string | null }
154
- source?: string
155
- createdOn?: string
156
- updatedOn?: string
157
- }
158
-
159
- export type KadimaAchStatus =
160
- | "Voided"
161
- | "Hold"
162
- | "Pending"
163
- | "Submitted"
164
- | "Transmitted"
165
- | "Settled"
166
- | "Returned"