@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.
- package/LICENSE +201 -0
- package/README.md +945 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +222 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +355 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +78 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +580 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/banks.d.ts +14 -0
- package/dist/component/banks.d.ts.map +1 -0
- package/dist/component/banks.js +37 -0
- package/dist/component/banks.js.map +1 -0
- package/dist/component/cashApi.d.ts +59 -0
- package/dist/component/cashApi.d.ts.map +1 -0
- package/dist/component/cashApi.js +88 -0
- package/dist/component/cashApi.js.map +1 -0
- package/dist/component/chargesApi.d.ts +64 -0
- package/dist/component/chargesApi.d.ts.map +1 -0
- package/dist/component/chargesApi.js +106 -0
- package/dist/component/chargesApi.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +6 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/customers.d.ts +67 -0
- package/dist/component/customers.d.ts.map +1 -0
- package/dist/component/customers.js +103 -0
- package/dist/component/customers.js.map +1 -0
- package/dist/component/customersApi.d.ts +99 -0
- package/dist/component/customersApi.d.ts.map +1 -0
- package/dist/component/customersApi.js +176 -0
- package/dist/component/customersApi.js.map +1 -0
- package/dist/component/daviplataApi.d.ts +43 -0
- package/dist/component/daviplataApi.d.ts.map +1 -0
- package/dist/component/daviplataApi.js +103 -0
- package/dist/component/daviplataApi.js.map +1 -0
- package/dist/component/epaycoClient.d.ts +84 -0
- package/dist/component/epaycoClient.d.ts.map +1 -0
- package/dist/component/epaycoClient.js +422 -0
- package/dist/component/epaycoClient.js.map +1 -0
- package/dist/component/payloads.d.ts +34 -0
- package/dist/component/payloads.d.ts.map +1 -0
- package/dist/component/payloads.js +45 -0
- package/dist/component/payloads.js.map +1 -0
- package/dist/component/plans.d.ts +47 -0
- package/dist/component/plans.d.ts.map +1 -0
- package/dist/component/plans.js +83 -0
- package/dist/component/plans.js.map +1 -0
- package/dist/component/plansApi.d.ts +64 -0
- package/dist/component/plansApi.d.ts.map +1 -0
- package/dist/component/plansApi.js +121 -0
- package/dist/component/plansApi.js.map +1 -0
- package/dist/component/pseApi.d.ts +68 -0
- package/dist/component/pseApi.d.ts.map +1 -0
- package/dist/component/pseApi.js +113 -0
- package/dist/component/pseApi.js.map +1 -0
- package/dist/component/rateLimits.d.ts +69 -0
- package/dist/component/rateLimits.d.ts.map +1 -0
- package/dist/component/rateLimits.js +67 -0
- package/dist/component/rateLimits.js.map +1 -0
- package/dist/component/safetypayApi.d.ts +35 -0
- package/dist/component/safetypayApi.d.ts.map +1 -0
- package/dist/component/safetypayApi.js +68 -0
- package/dist/component/safetypayApi.js.map +1 -0
- package/dist/component/schema.d.ts +200 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +104 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/signature.d.ts +11 -0
- package/dist/component/signature.d.ts.map +1 -0
- package/dist/component/signature.js +28 -0
- package/dist/component/signature.js.map +1 -0
- package/dist/component/status.d.ts +12 -0
- package/dist/component/status.d.ts.map +1 -0
- package/dist/component/status.js +55 -0
- package/dist/component/status.js.map +1 -0
- package/dist/component/subscriptions.d.ts +69 -0
- package/dist/component/subscriptions.d.ts.map +1 -0
- package/dist/component/subscriptions.js +114 -0
- package/dist/component/subscriptions.js.map +1 -0
- package/dist/component/subscriptionsApi.d.ts +62 -0
- package/dist/component/subscriptionsApi.d.ts.map +1 -0
- package/dist/component/subscriptionsApi.js +147 -0
- package/dist/component/subscriptionsApi.js.map +1 -0
- package/dist/component/tokens.d.ts +31 -0
- package/dist/component/tokens.d.ts.map +1 -0
- package/dist/component/tokens.js +79 -0
- package/dist/component/tokens.js.map +1 -0
- package/dist/component/tokensApi.d.ts +18 -0
- package/dist/component/tokensApi.d.ts.map +1 -0
- package/dist/component/tokensApi.js +53 -0
- package/dist/component/tokensApi.js.map +1 -0
- package/dist/component/transactions.d.ts +103 -0
- package/dist/component/transactions.d.ts.map +1 -0
- package/dist/component/transactions.js +177 -0
- package/dist/component/transactions.js.map +1 -0
- package/dist/component/validators.d.ts +571 -0
- package/dist/component/validators.d.ts.map +1 -0
- package/dist/component/validators.js +203 -0
- package/dist/component/validators.js.map +1 -0
- package/dist/component/webhooks.d.ts +55 -0
- package/dist/component/webhooks.d.ts.map +1 -0
- package/dist/component/webhooks.js +172 -0
- package/dist/component/webhooks.js.map +1 -0
- package/dist/react/index.d.ts +16 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +43 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +106 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.test.ts +66 -0
- package/src/client/index.ts +633 -0
- package/src/client/setup.test.ts +26 -0
- package/src/component/_generated/api.ts +94 -0
- package/src/component/_generated/component.ts +809 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/banks.ts +41 -0
- package/src/component/cashApi.ts +100 -0
- package/src/component/chargesApi.ts +119 -0
- package/src/component/convex.config.ts +7 -0
- package/src/component/customers.test.ts +122 -0
- package/src/component/customers.ts +116 -0
- package/src/component/customersApi.ts +206 -0
- package/src/component/daviplataApi.ts +119 -0
- package/src/component/epaycoApi.test.ts +110 -0
- package/src/component/epaycoClient.ts +578 -0
- package/src/component/payloads.ts +67 -0
- package/src/component/plans.test.ts +129 -0
- package/src/component/plans.ts +86 -0
- package/src/component/plansApi.ts +135 -0
- package/src/component/pseApi.ts +125 -0
- package/src/component/rateLimits.ts +67 -0
- package/src/component/safetypayApi.ts +78 -0
- package/src/component/schema.ts +124 -0
- package/src/component/setup.test.helper.ts +10 -0
- package/src/component/setup.test.ts +22 -0
- package/src/component/signature.ts +38 -0
- package/src/component/status.ts +71 -0
- package/src/component/subscriptions.test.ts +117 -0
- package/src/component/subscriptions.ts +128 -0
- package/src/component/subscriptionsApi.ts +172 -0
- package/src/component/tokens.ts +89 -0
- package/src/component/tokensApi.ts +63 -0
- package/src/component/transactions.test.ts +227 -0
- package/src/component/transactions.ts +200 -0
- package/src/component/validators.ts +245 -0
- package/src/component/webhooks.test.ts +137 -0
- package/src/component/webhooks.ts +229 -0
- package/src/react/index.ts +71 -0
- 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
|
+
}
|