@pulgueta/epayco-convex 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.
Files changed (169) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +945 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/index.d.ts +222 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +355 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/component/_generated/api.d.ts +78 -0
  12. package/dist/component/_generated/api.d.ts.map +1 -0
  13. package/dist/component/_generated/api.js +31 -0
  14. package/dist/component/_generated/api.js.map +1 -0
  15. package/dist/component/_generated/component.d.ts +580 -0
  16. package/dist/component/_generated/component.d.ts.map +1 -0
  17. package/dist/component/_generated/component.js +11 -0
  18. package/dist/component/_generated/component.js.map +1 -0
  19. package/dist/component/_generated/dataModel.d.ts +46 -0
  20. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  21. package/dist/component/_generated/dataModel.js +11 -0
  22. package/dist/component/_generated/dataModel.js.map +1 -0
  23. package/dist/component/_generated/server.d.ts +121 -0
  24. package/dist/component/_generated/server.d.ts.map +1 -0
  25. package/dist/component/_generated/server.js +78 -0
  26. package/dist/component/_generated/server.js.map +1 -0
  27. package/dist/component/banks.d.ts +14 -0
  28. package/dist/component/banks.d.ts.map +1 -0
  29. package/dist/component/banks.js +37 -0
  30. package/dist/component/banks.js.map +1 -0
  31. package/dist/component/cashApi.d.ts +59 -0
  32. package/dist/component/cashApi.d.ts.map +1 -0
  33. package/dist/component/cashApi.js +88 -0
  34. package/dist/component/cashApi.js.map +1 -0
  35. package/dist/component/chargesApi.d.ts +64 -0
  36. package/dist/component/chargesApi.d.ts.map +1 -0
  37. package/dist/component/chargesApi.js +106 -0
  38. package/dist/component/chargesApi.js.map +1 -0
  39. package/dist/component/convex.config.d.ts +3 -0
  40. package/dist/component/convex.config.d.ts.map +1 -0
  41. package/dist/component/convex.config.js +6 -0
  42. package/dist/component/convex.config.js.map +1 -0
  43. package/dist/component/customers.d.ts +67 -0
  44. package/dist/component/customers.d.ts.map +1 -0
  45. package/dist/component/customers.js +103 -0
  46. package/dist/component/customers.js.map +1 -0
  47. package/dist/component/customersApi.d.ts +99 -0
  48. package/dist/component/customersApi.d.ts.map +1 -0
  49. package/dist/component/customersApi.js +176 -0
  50. package/dist/component/customersApi.js.map +1 -0
  51. package/dist/component/daviplataApi.d.ts +43 -0
  52. package/dist/component/daviplataApi.d.ts.map +1 -0
  53. package/dist/component/daviplataApi.js +103 -0
  54. package/dist/component/daviplataApi.js.map +1 -0
  55. package/dist/component/epaycoClient.d.ts +84 -0
  56. package/dist/component/epaycoClient.d.ts.map +1 -0
  57. package/dist/component/epaycoClient.js +422 -0
  58. package/dist/component/epaycoClient.js.map +1 -0
  59. package/dist/component/payloads.d.ts +34 -0
  60. package/dist/component/payloads.d.ts.map +1 -0
  61. package/dist/component/payloads.js +45 -0
  62. package/dist/component/payloads.js.map +1 -0
  63. package/dist/component/plans.d.ts +47 -0
  64. package/dist/component/plans.d.ts.map +1 -0
  65. package/dist/component/plans.js +83 -0
  66. package/dist/component/plans.js.map +1 -0
  67. package/dist/component/plansApi.d.ts +64 -0
  68. package/dist/component/plansApi.d.ts.map +1 -0
  69. package/dist/component/plansApi.js +121 -0
  70. package/dist/component/plansApi.js.map +1 -0
  71. package/dist/component/pseApi.d.ts +68 -0
  72. package/dist/component/pseApi.d.ts.map +1 -0
  73. package/dist/component/pseApi.js +113 -0
  74. package/dist/component/pseApi.js.map +1 -0
  75. package/dist/component/rateLimits.d.ts +69 -0
  76. package/dist/component/rateLimits.d.ts.map +1 -0
  77. package/dist/component/rateLimits.js +67 -0
  78. package/dist/component/rateLimits.js.map +1 -0
  79. package/dist/component/safetypayApi.d.ts +35 -0
  80. package/dist/component/safetypayApi.d.ts.map +1 -0
  81. package/dist/component/safetypayApi.js +68 -0
  82. package/dist/component/safetypayApi.js.map +1 -0
  83. package/dist/component/schema.d.ts +200 -0
  84. package/dist/component/schema.d.ts.map +1 -0
  85. package/dist/component/schema.js +104 -0
  86. package/dist/component/schema.js.map +1 -0
  87. package/dist/component/signature.d.ts +11 -0
  88. package/dist/component/signature.d.ts.map +1 -0
  89. package/dist/component/signature.js +28 -0
  90. package/dist/component/signature.js.map +1 -0
  91. package/dist/component/status.d.ts +12 -0
  92. package/dist/component/status.d.ts.map +1 -0
  93. package/dist/component/status.js +55 -0
  94. package/dist/component/status.js.map +1 -0
  95. package/dist/component/subscriptions.d.ts +69 -0
  96. package/dist/component/subscriptions.d.ts.map +1 -0
  97. package/dist/component/subscriptions.js +114 -0
  98. package/dist/component/subscriptions.js.map +1 -0
  99. package/dist/component/subscriptionsApi.d.ts +62 -0
  100. package/dist/component/subscriptionsApi.d.ts.map +1 -0
  101. package/dist/component/subscriptionsApi.js +147 -0
  102. package/dist/component/subscriptionsApi.js.map +1 -0
  103. package/dist/component/tokens.d.ts +31 -0
  104. package/dist/component/tokens.d.ts.map +1 -0
  105. package/dist/component/tokens.js +79 -0
  106. package/dist/component/tokens.js.map +1 -0
  107. package/dist/component/tokensApi.d.ts +18 -0
  108. package/dist/component/tokensApi.d.ts.map +1 -0
  109. package/dist/component/tokensApi.js +53 -0
  110. package/dist/component/tokensApi.js.map +1 -0
  111. package/dist/component/transactions.d.ts +103 -0
  112. package/dist/component/transactions.d.ts.map +1 -0
  113. package/dist/component/transactions.js +177 -0
  114. package/dist/component/transactions.js.map +1 -0
  115. package/dist/component/validators.d.ts +571 -0
  116. package/dist/component/validators.d.ts.map +1 -0
  117. package/dist/component/validators.js +203 -0
  118. package/dist/component/validators.js.map +1 -0
  119. package/dist/component/webhooks.d.ts +55 -0
  120. package/dist/component/webhooks.d.ts.map +1 -0
  121. package/dist/component/webhooks.js +172 -0
  122. package/dist/component/webhooks.js.map +1 -0
  123. package/dist/react/index.d.ts +16 -0
  124. package/dist/react/index.d.ts.map +1 -0
  125. package/dist/react/index.js +43 -0
  126. package/dist/react/index.js.map +1 -0
  127. package/package.json +106 -0
  128. package/src/client/_generated/_ignore.ts +1 -0
  129. package/src/client/index.test.ts +66 -0
  130. package/src/client/index.ts +633 -0
  131. package/src/client/setup.test.ts +26 -0
  132. package/src/component/_generated/api.ts +94 -0
  133. package/src/component/_generated/component.ts +809 -0
  134. package/src/component/_generated/dataModel.ts +60 -0
  135. package/src/component/_generated/server.ts +156 -0
  136. package/src/component/banks.ts +41 -0
  137. package/src/component/cashApi.ts +100 -0
  138. package/src/component/chargesApi.ts +119 -0
  139. package/src/component/convex.config.ts +7 -0
  140. package/src/component/customers.test.ts +122 -0
  141. package/src/component/customers.ts +116 -0
  142. package/src/component/customersApi.ts +206 -0
  143. package/src/component/daviplataApi.ts +119 -0
  144. package/src/component/epaycoApi.test.ts +110 -0
  145. package/src/component/epaycoClient.ts +578 -0
  146. package/src/component/payloads.ts +67 -0
  147. package/src/component/plans.test.ts +129 -0
  148. package/src/component/plans.ts +86 -0
  149. package/src/component/plansApi.ts +135 -0
  150. package/src/component/pseApi.ts +125 -0
  151. package/src/component/rateLimits.ts +67 -0
  152. package/src/component/safetypayApi.ts +78 -0
  153. package/src/component/schema.ts +124 -0
  154. package/src/component/setup.test.helper.ts +10 -0
  155. package/src/component/setup.test.ts +22 -0
  156. package/src/component/signature.ts +38 -0
  157. package/src/component/status.ts +71 -0
  158. package/src/component/subscriptions.test.ts +117 -0
  159. package/src/component/subscriptions.ts +128 -0
  160. package/src/component/subscriptionsApi.ts +172 -0
  161. package/src/component/tokens.ts +89 -0
  162. package/src/component/tokensApi.ts +63 -0
  163. package/src/component/transactions.test.ts +227 -0
  164. package/src/component/transactions.ts +200 -0
  165. package/src/component/validators.ts +245 -0
  166. package/src/component/webhooks.test.ts +137 -0
  167. package/src/component/webhooks.ts +229 -0
  168. package/src/react/index.ts +71 -0
  169. package/src/test.ts +13 -0
@@ -0,0 +1,578 @@
1
+ import { ConvexError } from "convex/values";
2
+ import type { Value } from "convex/values";
3
+ import type { EPaycoCredentials } from "./validators.js";
4
+
5
+ /**
6
+ * Native (V8-runtime) implementation of the ePayco wire protocol.
7
+ *
8
+ * Convex components cannot use the Node.js runtime, so the official
9
+ * `epayco-sdk-node` package (which depends on Node built-ins) cannot run here.
10
+ * This module reproduces that SDK's exact request protocol using only Web
11
+ * platform APIs (`fetch`, Web Crypto, `TextEncoder`):
12
+ *
13
+ * - JWT login (`POST /v1/auth/login`) for `api.secure.payco.co` endpoints,
14
+ * and Basic-auth login (`POST /login`) for `apify.epayco.co` endpoints.
15
+ * - AES-128-CBC encryption (16-byte hex private key, all-zero IV, PKCS#7)
16
+ * for `secure.payco.co/restpagos` (PSE) endpoints, plus the
17
+ * `public_key` / `i` / `p` / `enpruebas` envelope the SDK emits.
18
+ * - Spanish field-name translation (`keylang`) for restpagos endpoints and
19
+ * camelCase translation (`keylang_apify`) for apify endpoints.
20
+ *
21
+ * Ported field-for-field from `epayco-sdk-node@1.4.4` `lib/resources/index.js`.
22
+ */
23
+
24
+ const BASE_URL = "https://api.secure.payco.co";
25
+ const SECURE_URL = "https://secure.payco.co";
26
+ const APIFY_URL = "https://apify.epayco.co";
27
+
28
+ /** Upper bound for any ePayco API call, so a hung host can't block the action. */
29
+ const REQUEST_TIMEOUT_MS = 30_000;
30
+ /** Tighter bound for the best-effort ipify lookup on the payment hot path. */
31
+ const IP_LOOKUP_TIMEOUT_MS = 5_000;
32
+
33
+ /** Base64 of a 16-byte all-zero IV (`i` for the encrypted restpagos envelope). */
34
+ const ZERO_IV_BASE64 = "AAAAAAAAAAAAAAAAAAAAAA==";
35
+ /** Base64 of the ASCII string "0000000000000000" (`i` for the cash envelope). */
36
+ const CASH_IV_BASE64 = "MDAwMDAwMDAwMDAwMDAwMA==";
37
+
38
+ // keylang.json — SDK field name -> restpagos (Spanish) wire field name.
39
+ const KEYLANG: Record<string, string> = {
40
+ bank: "banco",
41
+ invoice: "factura",
42
+ description: "descripcion",
43
+ value: "valor",
44
+ tax: "iva",
45
+ ico: "ico",
46
+ tax_base: "baseiva",
47
+ currency: "moneda",
48
+ type_person: "tipo_persona",
49
+ doc_type: "tipo_doc",
50
+ doc_number: "documento",
51
+ name: "nombres",
52
+ last_name: "apellidos",
53
+ email: "email",
54
+ country: "pais",
55
+ department: "depto",
56
+ city: "ciudad",
57
+ phone: "telefono",
58
+ cell_phone: "celular",
59
+ address: "direccion",
60
+ ip: "ip",
61
+ url_response: "url_respuesta",
62
+ url_confirmation: "url_confirmacion",
63
+ method_confirmation: "method_confirmation",
64
+ metodoconfirmacion: "metodoconfirmacion",
65
+ end_date: "fechaexpiracion",
66
+ };
67
+
68
+ // keylang_apify.json — SDK field name -> apify (camelCase) wire field name.
69
+ const KEYLANG_APIFY: Record<string, string> = {
70
+ cash: "cash",
71
+ end_date: "expirationDate",
72
+ ref_payco: "refPayco",
73
+ id_session_token: "idSessionToken",
74
+ otp: "otp",
75
+ invoice: "invoice",
76
+ description: "description",
77
+ value: "value",
78
+ tax: "tax",
79
+ ico: "ico",
80
+ tax_base: "taxBase",
81
+ currency: "currency",
82
+ doc_type: "docType",
83
+ doc_number: "document",
84
+ name: "name",
85
+ last_name: "lastName",
86
+ email: "email",
87
+ ind_country: "indCountry",
88
+ country: "country",
89
+ city: "city",
90
+ phone: "phone",
91
+ address: "address",
92
+ ip: "ip",
93
+ test: "testMode",
94
+ url_response: "urlResponse",
95
+ url_confirmation: "urlConfirmation",
96
+ method_confirmation: "methodConfirmation",
97
+ };
98
+
99
+ function langkey(key: string): string {
100
+ return KEYLANG[key] ?? key;
101
+ }
102
+ function langkeyApify(key: string): string {
103
+ return KEYLANG_APIFY[key] ?? key;
104
+ }
105
+
106
+ const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
107
+
108
+ function bytesToBase64(bytes: Uint8Array): string {
109
+ let out = "";
110
+ let i = 0;
111
+ for (; i + 3 <= bytes.length; i += 3) {
112
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
113
+ out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63] + B64[(n >> 6) & 63] + B64[n & 63];
114
+ }
115
+ const rem = bytes.length - i;
116
+ if (rem === 1) {
117
+ const n = bytes[i] << 16;
118
+ out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63] + "==";
119
+ } else if (rem === 2) {
120
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
121
+ out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63] + B64[(n >> 6) & 63] + "=";
122
+ }
123
+ return out;
124
+ }
125
+
126
+ function hexToBytes(hex: string): Uint8Array {
127
+ const bytes = new Uint8Array(Math.floor(hex.length / 2));
128
+ for (let i = 0; i < bytes.length; i++) {
129
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
130
+ }
131
+ return bytes;
132
+ }
133
+
134
+ function utf8ToBase64(str: string): string {
135
+ return bytesToBase64(new TextEncoder().encode(str));
136
+ }
137
+
138
+ /**
139
+ * AES-128-CBC encrypt a UTF-8 string with the hex private key and a zero IV,
140
+ * returning Base64 ciphertext. Web Crypto applies PKCS#7 padding by default,
141
+ * matching `crypto-js`'s `Pkcs7`. (A 16-byte/32-hex private key yields a valid
142
+ * AES-128 key.)
143
+ */
144
+ export async function aesEncrypt(
145
+ value: string,
146
+ privateKeyHex: string,
147
+ ): Promise<string> {
148
+ const key = await crypto.subtle.importKey(
149
+ "raw",
150
+ hexToBytes(privateKeyHex) as BufferSource,
151
+ { name: "AES-CBC" },
152
+ false,
153
+ ["encrypt"],
154
+ );
155
+ const ciphertext = await crypto.subtle.encrypt(
156
+ { name: "AES-CBC", iv: new Uint8Array(16) as BufferSource },
157
+ key,
158
+ new TextEncoder().encode(value) as BufferSource,
159
+ );
160
+ return bytesToBase64(new Uint8Array(ciphertext));
161
+ }
162
+
163
+ export type EpaycoResponse = Record<string, unknown> & {
164
+ success?: boolean;
165
+ status?: boolean | string;
166
+ titleResponse?: string;
167
+ title_response?: string;
168
+ textResponse?: string;
169
+ text_response?: string;
170
+ message?: string;
171
+ error?: string;
172
+ data?: Record<string, unknown>;
173
+ };
174
+
175
+ type Payload = Record<string, unknown>;
176
+
177
+ type RequestFlags = {
178
+ sw?: boolean;
179
+ cashData?: boolean;
180
+ card?: boolean;
181
+ apify?: boolean;
182
+ };
183
+
184
+ export interface EpaycoClient {
185
+ token: { create(options: Payload): Promise<EpaycoResponse> };
186
+ customers: {
187
+ create(options: Payload): Promise<EpaycoResponse>;
188
+ get(uid: string): Promise<EpaycoResponse>;
189
+ list(options?: Payload): Promise<EpaycoResponse>;
190
+ update(uid: string, options: Payload): Promise<EpaycoResponse>;
191
+ delete(options: Payload): Promise<EpaycoResponse>;
192
+ addDefaultCard(options: Payload): Promise<EpaycoResponse>;
193
+ addNewToken(options: Payload): Promise<EpaycoResponse>;
194
+ };
195
+ plans: {
196
+ create(options: Payload): Promise<EpaycoResponse>;
197
+ get(uid: string): Promise<EpaycoResponse>;
198
+ list(): Promise<EpaycoResponse>;
199
+ update(uid: string, options: Payload): Promise<EpaycoResponse>;
200
+ delete(uid: string): Promise<EpaycoResponse>;
201
+ };
202
+ subscriptions: {
203
+ create(options: Payload): Promise<EpaycoResponse>;
204
+ get(uid: string): Promise<EpaycoResponse>;
205
+ list(): Promise<EpaycoResponse>;
206
+ cancel(uid: string): Promise<EpaycoResponse>;
207
+ charge(options: Payload): Promise<EpaycoResponse>;
208
+ };
209
+ charge: {
210
+ create(options: Payload): Promise<EpaycoResponse>;
211
+ get(uid: string): Promise<EpaycoResponse>;
212
+ };
213
+ bank: {
214
+ create(options: Payload): Promise<EpaycoResponse>;
215
+ get(uid: string): Promise<EpaycoResponse>;
216
+ getBanks(): Promise<EpaycoResponse>;
217
+ };
218
+ cash: {
219
+ create(type: string, options: Payload): Promise<EpaycoResponse>;
220
+ get(uid: string): Promise<EpaycoResponse>;
221
+ };
222
+ daviplata: {
223
+ create(options: Payload): Promise<EpaycoResponse>;
224
+ confirm(options: Payload): Promise<EpaycoResponse>;
225
+ };
226
+ safetypay: { create(options: Payload): Promise<EpaycoResponse> };
227
+ }
228
+
229
+ /**
230
+ * Build an ePayco client bound to a set of credentials. Mirrors the resource
231
+ * method surface of `epayco-sdk-node` so the resource action files are written
232
+ * against the same API.
233
+ */
234
+ export function getEpaycoClient(credentials: EPaycoCredentials): EpaycoClient {
235
+ const apiKey = credentials.apiKey;
236
+ const privateKey = credentials.privateKey;
237
+ const test = credentials.testMode ?? false ? "TRUE" : "FALSE";
238
+
239
+ async function authenticate(apify: boolean): Promise<string> {
240
+ if (apify) {
241
+ const basic = utf8ToBase64(`${apiKey}:${privateKey}`);
242
+ const res = await fetch(`${APIFY_URL}/login`, {
243
+ method: "POST",
244
+ headers: {
245
+ "Content-Type": "application/json",
246
+ // The trailing ";" is intentional: it matches epayco-sdk-node@1.4.4
247
+ // (lib/resources/index.js) exactly, which the apify host expects.
248
+ Authorization: `Basic ${basic};`,
249
+ },
250
+ body: JSON.stringify({}),
251
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
252
+ });
253
+ const json = (await res.json()) as Record<string, unknown>;
254
+ return String(json.token ?? json.bearer_token ?? "");
255
+ }
256
+ const res = await fetch(`${BASE_URL}/v1/auth/login`, {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/json" },
259
+ body: JSON.stringify({ public_key: apiKey, private_key: privateKey }),
260
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
261
+ });
262
+ const json = (await res.json()) as Record<string, unknown>;
263
+ return String(json.bearer_token ?? json.token ?? "");
264
+ }
265
+
266
+ async function setData(
267
+ data: Payload,
268
+ cashData: boolean,
269
+ apify: boolean,
270
+ ): Promise<Payload> {
271
+ const set: Payload = {};
272
+ if (apify) {
273
+ for (const key of Object.keys(data)) {
274
+ set[langkeyApify(key)] = data[key];
275
+ }
276
+ return set;
277
+ }
278
+ if (cashData) {
279
+ for (const key of Object.keys(data)) {
280
+ set[langkey(key)] = data[key];
281
+ }
282
+ set.public_key = apiKey;
283
+ set.i = CASH_IV_BASE64;
284
+ set.enpruebas = test;
285
+ set.lenguaje = "javascript";
286
+ set.p = "";
287
+ return set;
288
+ }
289
+ // Encrypted (PSE) path.
290
+ for (const key of Object.keys(data)) {
291
+ const value = data[key];
292
+ if (key.includes("extras_epayco") && value && typeof value === "object") {
293
+ const extra5 = (value as Record<string, unknown>).extra5;
294
+ set[langkey(key)] = {
295
+ extra5: await aesEncrypt(String(extra5 ?? ""), privateKey),
296
+ };
297
+ } else {
298
+ set[langkey(key)] = await aesEncrypt(String(value), privateKey);
299
+ }
300
+ }
301
+ set.public_key = apiKey;
302
+ set.i = ZERO_IV_BASE64;
303
+ set.enpruebas = await aesEncrypt(test, privateKey);
304
+ set.lenguaje = "javascript";
305
+ set.p = bytesToBase64(hexToBytes(privateKey));
306
+ return set;
307
+ }
308
+
309
+ async function request(
310
+ method: "get" | "post",
311
+ path: string,
312
+ data: Payload,
313
+ flags: RequestFlags = {},
314
+ ): Promise<EpaycoResponse> {
315
+ const { sw = false, cashData = false, card = false, apify = false } = flags;
316
+ const bearer = `Bearer ${await authenticate(apify)}`;
317
+
318
+ const headers: Record<string, string> = {
319
+ "Content-Type": "application/json",
320
+ type: "sdk-jwt",
321
+ lang: "NODE",
322
+ Authorization: bearer,
323
+ };
324
+
325
+ const base = apify ? APIFY_URL : sw ? SECURE_URL : BASE_URL;
326
+ const url = base + path;
327
+
328
+ if (method === "get") {
329
+ const res = await fetch(url, {
330
+ method: "GET",
331
+ headers,
332
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
333
+ });
334
+ return parseResponse(res);
335
+ }
336
+
337
+ let body: Payload = { ...data, extras_epayco: { extra5: "P44" } };
338
+ if (!card) {
339
+ body.test = test;
340
+ if (body.ip === undefined) {
341
+ const ip = await fetchServerIp();
342
+ if (ip) body.ip = ip;
343
+ }
344
+ }
345
+ if (sw || apify) {
346
+ body = await setData(body, cashData, apify);
347
+ }
348
+
349
+ const res = await fetch(url, {
350
+ method: "POST",
351
+ headers,
352
+ body: JSON.stringify(body),
353
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
354
+ });
355
+ return parseResponse(res);
356
+ }
357
+
358
+ const txLookup = (uid: string) =>
359
+ request(
360
+ "get",
361
+ `/restpagos/transaction/response.json?ref_payco=${encodeURIComponent(uid)}&&public_key=${apiKey}`,
362
+ {},
363
+ { sw: true },
364
+ );
365
+
366
+ return {
367
+ token: {
368
+ create: (options) => request("post", "/v1/tokens", options),
369
+ },
370
+ customers: {
371
+ create: (options) =>
372
+ request("post", "/payment/v1/customer/create", options),
373
+ get: (uid) =>
374
+ request("get", `/payment/v1/customer/${apiKey}/${uid}`, {}),
375
+ list: (options) =>
376
+ request("get", buildListUrl("/payment/v1/customers", options), {}, {
377
+ card: true,
378
+ }),
379
+ update: (uid, options) =>
380
+ request("post", `/payment/v1/customer/edit/${apiKey}/${uid}`, options),
381
+ delete: (options) => request("post", "/v1/remove/token", options),
382
+ addDefaultCard: (options) =>
383
+ request("post", "/payment/v1/customer/reasign/card/default", options, {
384
+ card: true,
385
+ }),
386
+ addNewToken: (options) =>
387
+ request("post", "/v1/customer/add/token", options, { card: true }),
388
+ },
389
+ plans: {
390
+ create: (options) => request("post", "/recurring/v1/plan/create", options),
391
+ get: (uid) => request("get", `/recurring/v1/plan/${apiKey}/${uid}`, {}),
392
+ list: () => request("get", `/recurring/v1/plans/${apiKey}`, {}),
393
+ update: (uid, options) =>
394
+ request("post", `/recurring/v1/plan/edit/${uid}`, options),
395
+ delete: (uid) =>
396
+ request("post", `/recurring/v1/plan/remove/${apiKey}/${uid}`, {}),
397
+ },
398
+ subscriptions: {
399
+ create: (options) =>
400
+ request("post", "/recurring/v1/subscription/create", options),
401
+ get: (uid) =>
402
+ request("get", `/recurring/v1/subscription/${uid}/${apiKey}`, {}),
403
+ list: () => request("get", `/recurring/v1/subscriptions/${apiKey}`, {}),
404
+ cancel: (uid) =>
405
+ request("post", "/recurring/v1/subscription/cancel", {
406
+ id: uid,
407
+ public_key: apiKey,
408
+ }),
409
+ charge: (options) =>
410
+ request("post", "/payment/v1/charge/subscription/create", options),
411
+ },
412
+ charge: {
413
+ create: (options) => request("post", "/payment/v1/charge/create", options),
414
+ get: (uid) => txLookup(uid),
415
+ },
416
+ bank: {
417
+ create: (options) =>
418
+ request("post", "/restpagos/pagos/debitos.json", options, { sw: true }),
419
+ get: (uid) =>
420
+ request(
421
+ "get",
422
+ `/restpagos/pse/transactioninfomation.json?transactionID=${encodeURIComponent(uid)}&&public_key=${apiKey}`,
423
+ {},
424
+ { sw: true },
425
+ ),
426
+ getBanks: () =>
427
+ request(
428
+ "get",
429
+ `/restpagos/pse/bancos.json?public_key=${apiKey}`,
430
+ {},
431
+ { sw: true, cashData: true },
432
+ ),
433
+ },
434
+ cash: {
435
+ create: (type, options) =>
436
+ request("post", `/restpagos/v2/efectivo/${type}`, options, {
437
+ sw: true,
438
+ cashData: true,
439
+ }),
440
+ get: (uid) => txLookup(uid),
441
+ },
442
+ daviplata: {
443
+ create: (options) =>
444
+ request("post", "/payment/process/daviplata", options, {
445
+ apify: true,
446
+ card: true,
447
+ }),
448
+ confirm: (options) =>
449
+ request("post", "/payment/confirm/daviplata", options, {
450
+ apify: true,
451
+ card: true,
452
+ }),
453
+ },
454
+ safetypay: {
455
+ create: (options) =>
456
+ request("post", "/payment/process/safetypay", options, {
457
+ apify: true,
458
+ card: true,
459
+ }),
460
+ },
461
+ };
462
+ }
463
+
464
+ function buildListUrl(path: string, options?: Payload): string {
465
+ if (!options) return path;
466
+ const params = new URLSearchParams();
467
+ for (const [key, value] of Object.entries(options)) {
468
+ if (value !== undefined && value !== null) params.set(key, String(value));
469
+ }
470
+ const qs = params.toString();
471
+ return qs ? `${path}?${qs}` : path;
472
+ }
473
+
474
+ async function parseResponse(res: Response): Promise<EpaycoResponse> {
475
+ const text = await res.text();
476
+ try {
477
+ return JSON.parse(text) as EpaycoResponse;
478
+ } catch {
479
+ return { error: text || `HTTP ${res.status}` };
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Best-effort server IP lookup (matches the SDK's ipify fallback). Used only
485
+ * when the caller doesn't supply `ip`; bounded by a short timeout so the payment
486
+ * hot path can't stall on a slow third party, and failures fall through silently.
487
+ */
488
+ async function fetchServerIp(): Promise<string | undefined> {
489
+ try {
490
+ const res = await fetch("https://api.ipify.org?format=json", {
491
+ signal: AbortSignal.timeout(IP_LOOKUP_TIMEOUT_MS),
492
+ });
493
+ const json = (await res.json()) as { ip?: string };
494
+ return json.ip;
495
+ } catch {
496
+ return undefined;
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Normalize ePayco's response into either the raw JSON or a thrown
502
+ * `ConvexError`. ePayco never uses non-2xx for business errors — failures show
503
+ * up as `{ error }` or `{ success: false }` — so we surface those as throws.
504
+ */
505
+ export function unwrap(result: EpaycoResponse | null | undefined): EpaycoResponse {
506
+ if (result == null) {
507
+ throw new ConvexError({
508
+ code: "EPAYCO_API_ERROR",
509
+ message: "Empty response from ePayco",
510
+ });
511
+ }
512
+
513
+ // The SDK-style `{ error }` envelope, or ePayco's bare `{ message }` envelope
514
+ // (returned for account-gated features like recurring plans) — neither
515
+ // carries a success flag, payload, or id, so treat them as failures.
516
+ const hasNoSuccessSignal =
517
+ result.success === undefined &&
518
+ result.status === undefined &&
519
+ result.data === undefined &&
520
+ result.id === undefined &&
521
+ (result as Record<string, unknown>).ref_payco === undefined &&
522
+ (result as Record<string, unknown>).token === undefined;
523
+
524
+ if (typeof result.error === "string" && hasNoSuccessSignal) {
525
+ throw new ConvexError({
526
+ code: "EPAYCO_API_ERROR",
527
+ message: result.error,
528
+ raw: result as unknown as Value,
529
+ });
530
+ }
531
+
532
+ if (typeof result.message === "string" && hasNoSuccessSignal) {
533
+ throw new ConvexError({
534
+ code: "EPAYCO_API_ERROR",
535
+ message: result.message,
536
+ raw: result as unknown as Value,
537
+ });
538
+ }
539
+
540
+ if (result.success === false || result.status === false) {
541
+ const message =
542
+ result.text_response ??
543
+ result.textResponse ??
544
+ result.title_response ??
545
+ result.titleResponse ??
546
+ result.message ??
547
+ (typeof result.data === "object" && result.data !== null
548
+ ? (result.data as Record<string, unknown>).description
549
+ : undefined) ??
550
+ "ePayco request failed";
551
+ throw new ConvexError({
552
+ code: "EPAYCO_API_ERROR",
553
+ message: String(message),
554
+ raw: result as unknown as Value,
555
+ });
556
+ }
557
+
558
+ return result;
559
+ }
560
+
561
+ export function dataOf(result: EpaycoResponse): Record<string, unknown> {
562
+ return typeof result.data === "object" && result.data !== null
563
+ ? (result.data as Record<string, unknown>)
564
+ : {};
565
+ }
566
+
567
+ export function pick(
568
+ source: Record<string, unknown>,
569
+ keys: string[],
570
+ ): string | undefined {
571
+ for (const key of keys) {
572
+ const value = source[key];
573
+ if (value !== undefined && value !== null && value !== "") {
574
+ return String(value);
575
+ }
576
+ }
577
+ return undefined;
578
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Pure helpers for shaping ePayco request payloads. V8-safe (no SDK / Node
3
+ * imports) so they can be unit-tested and shared across action files.
4
+ */
5
+
6
+ export type SplitInput = {
7
+ splitType?: string;
8
+ splitAppId?: string;
9
+ splitMerchantId?: string;
10
+ splitPrimaryReceiver?: string;
11
+ splitPrimaryReceiverFee?: string;
12
+ splitRule?: string;
13
+ splitReceivers?: Array<{
14
+ id: string;
15
+ total: string;
16
+ iva: string;
17
+ base_iva: string;
18
+ fee?: string;
19
+ }>;
20
+ };
21
+
22
+ /**
23
+ * Build the `split_*` wire fields for a dispersion. `stringifyReceivers`
24
+ * controls whether `split_receivers` is sent as a JSON string (PSE / cash) or
25
+ * a raw array (credit-card charge), matching the official SDK examples.
26
+ */
27
+ export function buildSplitPayload(
28
+ split: SplitInput | undefined,
29
+ stringifyReceivers: boolean,
30
+ ): Record<string, unknown> {
31
+ if (!split) return {};
32
+ const out: Record<string, unknown> = { splitpayment: "true" };
33
+ if (split.splitType) out.split_type = split.splitType;
34
+ if (split.splitAppId) out.split_app_id = split.splitAppId;
35
+ if (split.splitMerchantId) out.split_merchant_id = split.splitMerchantId;
36
+ if (split.splitPrimaryReceiver)
37
+ out.split_primary_receiver = split.splitPrimaryReceiver;
38
+ if (split.splitPrimaryReceiverFee)
39
+ out.split_primary_receiver_fee = split.splitPrimaryReceiverFee;
40
+ if (split.splitRule) out.split_rule = split.splitRule;
41
+ if (split.splitReceivers && split.splitReceivers.length > 0) {
42
+ out.split_receivers = stringifyReceivers
43
+ ? JSON.stringify(split.splitReceivers)
44
+ : split.splitReceivers;
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /** Convert split receivers (string fields) into the numeric form we persist. */
50
+ export function storedReceivers(
51
+ split: SplitInput | undefined,
52
+ ): Array<{
53
+ id: string;
54
+ total: number;
55
+ iva: number;
56
+ base_iva: number;
57
+ fee?: number;
58
+ }> | undefined {
59
+ if (!split?.splitReceivers) return undefined;
60
+ return split.splitReceivers.map((r) => ({
61
+ id: r.id,
62
+ total: Number(r.total),
63
+ iva: Number(r.iva),
64
+ base_iva: Number(r.base_iva),
65
+ ...(r.fee !== undefined ? { fee: Number(r.fee) } : {}),
66
+ }));
67
+ }