@paykit-sdk/redsys 1.0.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/README.md +96 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +543 -0
- package/dist/index.mjs +539 -0
- package/dist/redsys-provider.d.mts +123 -0
- package/dist/redsys-provider.d.ts +123 -0
- package/dist/redsys-provider.js +522 -0
- package/dist/redsys-provider.mjs +520 -0
- package/package.json +41 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { schema, Schema, AbstractPayKitProvider, HTTPClient, NotImplementedError, createCheckoutSchema, ValidationError, validateRequiredKeys, isIdCustomer, isEmailCustomer, stringifyMetadataValues, PAYKIT_METADATA_KEY, ResourceNotFoundError, createPaymentSchema, OperationFailedError, ProviderNotSupportedError, createRefundSchema, WebhookError, parseJSON, paykitEvent$InboundSchema } from '@paykit-sdk/core';
|
|
2
|
+
import { randomBytes, createHmac } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var RedsysOptionsSchema = schema()(
|
|
6
|
+
Schema.object({
|
|
7
|
+
merchantCode: Schema.string().min(1),
|
|
8
|
+
terminal: Schema.string().min(1),
|
|
9
|
+
secretKey: Schema.string().min(1),
|
|
10
|
+
isSandbox: Schema.boolean(),
|
|
11
|
+
transactionType: Schema.enum(["0", "1"]).optional(),
|
|
12
|
+
redsysUrl: Schema.string().optional(),
|
|
13
|
+
debug: Schema.boolean().optional()
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
var PROVIDER_NAME = "redsys";
|
|
17
|
+
var CURRENCY_MAP = {
|
|
18
|
+
EUR: 978,
|
|
19
|
+
USD: 840,
|
|
20
|
+
GBP: 826,
|
|
21
|
+
JPY: 392
|
|
22
|
+
};
|
|
23
|
+
var RESPONSE_CODES = {
|
|
24
|
+
"0000": "Transaction approved",
|
|
25
|
+
"0101": "Card expired",
|
|
26
|
+
"0102": "Card temporarily blocked",
|
|
27
|
+
"0106": "Attempts exceeded",
|
|
28
|
+
"0116": "Insufficient balance",
|
|
29
|
+
"0129": "Invalid card security code",
|
|
30
|
+
"0184": "Cardholder authentication failed",
|
|
31
|
+
"0190": "Card issuer unavailable",
|
|
32
|
+
"0904": "Merchant not registered"
|
|
33
|
+
};
|
|
34
|
+
var RedsysProvider = class extends AbstractPayKitProvider {
|
|
35
|
+
opts;
|
|
36
|
+
_client;
|
|
37
|
+
isSandbox;
|
|
38
|
+
constructor(opts) {
|
|
39
|
+
super(RedsysOptionsSchema, opts, PROVIDER_NAME);
|
|
40
|
+
this.opts = opts;
|
|
41
|
+
this.isSandbox = opts.isSandbox ?? false;
|
|
42
|
+
this._client = new HTTPClient({
|
|
43
|
+
baseUrl: this._getRedsysUrl(),
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
retryOptions: { max: 3, baseDelay: 1e3, debug: false }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
providerName = PROVIDER_NAME;
|
|
49
|
+
get _native() {
|
|
50
|
+
throw new NotImplementedError(
|
|
51
|
+
"Native SDK not available for Redsys. Use inSite iframe.",
|
|
52
|
+
this.providerName,
|
|
53
|
+
{ futureSupport: true }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
_getRedsysUrl() {
|
|
57
|
+
if (this.opts.redsysUrl) return this.opts.redsysUrl;
|
|
58
|
+
return this.opts.isSandbox ? "https://sis.redsys.es/sis/realizarPago" : "https://sis-t.REDsys.es:25443/sis/realizarPago";
|
|
59
|
+
}
|
|
60
|
+
_getCurrencyNum(currency) {
|
|
61
|
+
return CURRENCY_MAP[currency.toUpperCase()] ?? 978;
|
|
62
|
+
}
|
|
63
|
+
_generateOrderId() {
|
|
64
|
+
return randomBytes(6).toString("hex").toUpperCase();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build the Base64-encoded merchant parameters for inSite
|
|
68
|
+
*/
|
|
69
|
+
_buildMerchantParams(orderId, amount, currency) {
|
|
70
|
+
const currencyNum = this._getCurrencyNum(currency);
|
|
71
|
+
const params = {
|
|
72
|
+
DS_MERCHANT_MERCHANTCODE: this.opts.merchantCode,
|
|
73
|
+
DS_MERCHANT_TERMINAL: this.opts.terminal,
|
|
74
|
+
DS_MERCHANT_ORDER: orderId,
|
|
75
|
+
DS_MERCHANT_AMOUNT: String(amount),
|
|
76
|
+
DS_MERCHANT_CURRENCY: String(currencyNum),
|
|
77
|
+
DS_MERCHANT_TRANSACTIONTYPE: this.opts.transactionType ?? "0",
|
|
78
|
+
DS_MERCHANT_CONSUMERLANGUAGE: "1",
|
|
79
|
+
DS_MERCHANT_PRODUCTDESCRIPTION: "Payment",
|
|
80
|
+
DS_MERCHANT_TITULAR: ""
|
|
81
|
+
};
|
|
82
|
+
const jsonStr = JSON.stringify(params);
|
|
83
|
+
return Buffer.from(jsonStr).toString("base64");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Generate HMAC-SHA256 signature for inSite
|
|
87
|
+
*
|
|
88
|
+
* Signature = HMAC-SHA256(secretKey, base64(params) + orderId)
|
|
89
|
+
* Order is important: base64Params + orderId (both as-is, no separators)
|
|
90
|
+
*/
|
|
91
|
+
_sign(orderId, base64Params) {
|
|
92
|
+
const data = base64Params + orderId;
|
|
93
|
+
return createHmac(
|
|
94
|
+
"sha256",
|
|
95
|
+
Buffer.from(this.opts.secretKey, "base64")
|
|
96
|
+
).update(data).digest("base64").replace(/\+/g, "-").replace(/\//g, "/").replace(/=+$/, "");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Create checkout for inSite
|
|
100
|
+
*
|
|
101
|
+
* Returns merchant parameters and signature for the frontend iframe.
|
|
102
|
+
* The frontend will load redsysV3.js and render the inSite form.
|
|
103
|
+
*/
|
|
104
|
+
createCheckout = async (params) => {
|
|
105
|
+
const { error, data } = createCheckoutSchema.safeParse(params);
|
|
106
|
+
if (error) {
|
|
107
|
+
throw ValidationError.fromZodError(
|
|
108
|
+
error,
|
|
109
|
+
this.providerName,
|
|
110
|
+
"createCheckout"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const { amount: rawAmount, currency = "EUR" } = validateRequiredKeys(
|
|
114
|
+
["amount", "currency"],
|
|
115
|
+
data.metadata ?? {},
|
|
116
|
+
"The following fields must be present in the metadata of createCheckout: {keys}"
|
|
117
|
+
);
|
|
118
|
+
const orderId = data.metadata?.orderId ?? this._generateOrderId();
|
|
119
|
+
const amount = Math.round(Number(rawAmount));
|
|
120
|
+
const merchantParams = this._buildMerchantParams(
|
|
121
|
+
orderId,
|
|
122
|
+
amount,
|
|
123
|
+
currency
|
|
124
|
+
);
|
|
125
|
+
const signature = this._sign(orderId, merchantParams);
|
|
126
|
+
let customer = null;
|
|
127
|
+
if (isIdCustomer(data.customer)) {
|
|
128
|
+
customer = { id: data.customer.id };
|
|
129
|
+
} else if (isEmailCustomer(data.customer)) {
|
|
130
|
+
customer = { email: data.customer.email };
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
id: `redsys_${orderId}`,
|
|
134
|
+
customer,
|
|
135
|
+
session_type: "one_time",
|
|
136
|
+
payment_url: this._getRedsysUrl(),
|
|
137
|
+
products: [{ id: data.item_id, quantity: data.quantity }],
|
|
138
|
+
currency,
|
|
139
|
+
amount,
|
|
140
|
+
subscription: null,
|
|
141
|
+
metadata: {
|
|
142
|
+
...stringifyMetadataValues(data.metadata ?? {}),
|
|
143
|
+
[PAYKIT_METADATA_KEY]: JSON.stringify({ orderId, currency }),
|
|
144
|
+
// These are passed to frontend for inSite initialization
|
|
145
|
+
redsys_merchant_params: merchantParams,
|
|
146
|
+
redsys_signature: signature,
|
|
147
|
+
redsys_signature_version: "HMAC_SHA256_V1",
|
|
148
|
+
redsys_merchant_code: this.opts.merchantCode,
|
|
149
|
+
redsys_terminal: this.opts.terminal
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
retrieveCheckout = async (id) => {
|
|
154
|
+
throw new ResourceNotFoundError(
|
|
155
|
+
"checkout",
|
|
156
|
+
id,
|
|
157
|
+
this.providerName
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
updateCheckout = async (id, _params) => {
|
|
161
|
+
throw new NotImplementedError(
|
|
162
|
+
"updateCheckout",
|
|
163
|
+
this.providerName,
|
|
164
|
+
{ futureSupport: true }
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
deleteCheckout = async (_id) => {
|
|
168
|
+
return null;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Create payment using operation ID from inSite
|
|
172
|
+
*
|
|
173
|
+
* The operation ID is returned by the inSite iframe when the user
|
|
174
|
+
* completes the card entry. The frontend should pass it in
|
|
175
|
+
* provider_metadata.operationId.
|
|
176
|
+
*/
|
|
177
|
+
createPayment = async (params) => {
|
|
178
|
+
const { error, data } = createPaymentSchema.safeParse(params);
|
|
179
|
+
if (error) {
|
|
180
|
+
throw ValidationError.fromZodError(
|
|
181
|
+
error,
|
|
182
|
+
this.providerName,
|
|
183
|
+
"createPayment"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const operationId = data.provider_metadata?.operationId;
|
|
187
|
+
if (!operationId) {
|
|
188
|
+
throw new ValidationError(
|
|
189
|
+
"operationId is required for Redsys inSite payments. Pass it in provider_metadata.operationId.",
|
|
190
|
+
{
|
|
191
|
+
provider: this.providerName,
|
|
192
|
+
method: "createPayment",
|
|
193
|
+
field: "operationId"
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const metadataItem = JSON.parse(
|
|
198
|
+
data.metadata?.[PAYKIT_METADATA_KEY] ?? "{}"
|
|
199
|
+
);
|
|
200
|
+
const orderId = metadataItem.orderId;
|
|
201
|
+
const result = await this._executePayment(
|
|
202
|
+
orderId,
|
|
203
|
+
operationId,
|
|
204
|
+
Number(data.amount),
|
|
205
|
+
String(data.currency)
|
|
206
|
+
);
|
|
207
|
+
if (result.Ds_Response === "0000" || String(result.Ds_Response).startsWith("00")) {
|
|
208
|
+
return {
|
|
209
|
+
id: result.Ds_AuthorisationCode ? `${orderId}_${result.Ds_AuthorisationCode}` : operationId,
|
|
210
|
+
amount: data.amount,
|
|
211
|
+
currency: data.currency,
|
|
212
|
+
customer: data.customer ? typeof data.customer === "string" ? { id: data.customer } : data.customer : null,
|
|
213
|
+
status: "succeeded",
|
|
214
|
+
metadata: stringifyMetadataValues(data.metadata ?? {}),
|
|
215
|
+
item_id: data.item_id,
|
|
216
|
+
requires_action: false,
|
|
217
|
+
payment_url: null
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
throw new OperationFailedError(
|
|
221
|
+
`Payment failed: ${RESPONSE_CODES[String(result.Ds_Response)] ?? `Code ${result.Ds_Response}`}`,
|
|
222
|
+
this.providerName,
|
|
223
|
+
{ cause: new Error(JSON.stringify(result)) }
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Execute payment via Redsys REST API
|
|
228
|
+
*/
|
|
229
|
+
async _executePayment(orderId, operationId, amount, currency) {
|
|
230
|
+
const currencyNum = this._getCurrencyNum(currency);
|
|
231
|
+
const amountStr = String(amount);
|
|
232
|
+
const params = {
|
|
233
|
+
DS_MERCHANT_MERCHANTCODE: this.opts.merchantCode,
|
|
234
|
+
DS_MERCHANT_TERMINAL: this.opts.terminal,
|
|
235
|
+
DS_MERCHANT_ORDER: orderId,
|
|
236
|
+
DS_MERCHANT_AMOUNT: amountStr,
|
|
237
|
+
DS_MERCHANT_CURRENCY: String(currencyNum),
|
|
238
|
+
DS_MERCHANT_TRANSACTIONTYPE: this.opts.transactionType ?? "0",
|
|
239
|
+
DS_MERCHANT_IDOPERACION: operationId
|
|
240
|
+
};
|
|
241
|
+
const base64Params = Buffer.from(JSON.stringify(params)).toString(
|
|
242
|
+
"base64"
|
|
243
|
+
);
|
|
244
|
+
const signature = this._sign(orderId, base64Params);
|
|
245
|
+
const { ok, value: result } = await this._client.post("/", {
|
|
246
|
+
body: JSON.stringify({
|
|
247
|
+
Ds_SignatureVersion: "HMAC_SHA256_V1",
|
|
248
|
+
Ds_MerchantParameters: base64Params,
|
|
249
|
+
Ds_Signature: signature
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
if (!ok || !result) {
|
|
253
|
+
throw new OperationFailedError(
|
|
254
|
+
"Redsys REST API error: request failed",
|
|
255
|
+
this.providerName
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
retrievePayment = async (id) => {
|
|
261
|
+
throw new NotImplementedError(
|
|
262
|
+
"retrievePayment",
|
|
263
|
+
this.providerName,
|
|
264
|
+
{
|
|
265
|
+
futureSupport: true
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
updatePayment = async (id, _params) => {
|
|
270
|
+
throw new NotImplementedError(
|
|
271
|
+
"updatePayment",
|
|
272
|
+
this.providerName,
|
|
273
|
+
{ futureSupport: true }
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
deletePayment = async (_id) => {
|
|
277
|
+
return null;
|
|
278
|
+
};
|
|
279
|
+
capturePayment = async (id, _params) => {
|
|
280
|
+
if (this.opts.transactionType === "1") {
|
|
281
|
+
throw new NotImplementedError(
|
|
282
|
+
"capturePayment",
|
|
283
|
+
this.providerName,
|
|
284
|
+
{ futureSupport: true }
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
throw new NotImplementedError(
|
|
288
|
+
"capturePayment only available for pre-authorization (transactionType=1)",
|
|
289
|
+
this.providerName
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
cancelPayment = async (id) => {
|
|
293
|
+
throw new NotImplementedError(
|
|
294
|
+
"cancelPayment",
|
|
295
|
+
this.providerName,
|
|
296
|
+
{ futureSupport: true }
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
createCustomer = async (params) => {
|
|
300
|
+
throw new ProviderNotSupportedError("createCustomer", "gopay", {
|
|
301
|
+
reason: "Redsys doesn't support creating customers"
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
retrieveCustomer = async (_id) => {
|
|
305
|
+
return null;
|
|
306
|
+
};
|
|
307
|
+
updateCustomer = async (id, _params) => {
|
|
308
|
+
throw new NotImplementedError(
|
|
309
|
+
"updateCustomer",
|
|
310
|
+
this.providerName,
|
|
311
|
+
{ futureSupport: true }
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
deleteCustomer = async (_id) => {
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
createSubscription = async (_params) => {
|
|
318
|
+
return this._notSupported("createSubscription");
|
|
319
|
+
};
|
|
320
|
+
retrieveSubscription = async (_id) => {
|
|
321
|
+
return this._notSupported("retrieveSubscription");
|
|
322
|
+
};
|
|
323
|
+
updateSubscription = async (_id, _params) => {
|
|
324
|
+
return this._notSupported("updateSubscription");
|
|
325
|
+
};
|
|
326
|
+
deleteSubscription = async (_id) => {
|
|
327
|
+
return this._notSupported("deleteSubscription");
|
|
328
|
+
};
|
|
329
|
+
cancelSubscription = async (_id) => {
|
|
330
|
+
return this._notSupported("cancelSubscription");
|
|
331
|
+
};
|
|
332
|
+
createRefund = async (params) => {
|
|
333
|
+
const { error, data } = createRefundSchema.safeParse(params);
|
|
334
|
+
if (error) {
|
|
335
|
+
throw ValidationError.fromZodError(
|
|
336
|
+
error,
|
|
337
|
+
this.providerName,
|
|
338
|
+
"createRefund"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const { currency = "EUR" } = validateRequiredKeys(
|
|
342
|
+
["currency"],
|
|
343
|
+
data.metadata ?? {},
|
|
344
|
+
"The following fields must be present in the metadata of createRefund: {keys}"
|
|
345
|
+
);
|
|
346
|
+
const metadataItem = JSON.parse(
|
|
347
|
+
data.metadata?.[PAYKIT_METADATA_KEY] ?? "{}"
|
|
348
|
+
);
|
|
349
|
+
const orderId = metadataItem.orderId;
|
|
350
|
+
if (!orderId) {
|
|
351
|
+
throw new ValidationError(
|
|
352
|
+
"orderId not found in payment metadata",
|
|
353
|
+
{
|
|
354
|
+
provider: this.providerName,
|
|
355
|
+
method: "createRefund",
|
|
356
|
+
field: "orderId"
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const result = await this._refundPayment(
|
|
361
|
+
orderId,
|
|
362
|
+
data.amount,
|
|
363
|
+
currency
|
|
364
|
+
);
|
|
365
|
+
const code = String(result.Ds_Response ?? result.Ds_Restock);
|
|
366
|
+
if (code === "0000" || code.startsWith("00")) {
|
|
367
|
+
return {
|
|
368
|
+
id: `refund_${randomBytes(4).toString("hex")}`,
|
|
369
|
+
amount: data.amount,
|
|
370
|
+
currency,
|
|
371
|
+
reason: "refund",
|
|
372
|
+
metadata: {}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
throw new OperationFailedError(
|
|
376
|
+
`Refund failed: ${RESPONSE_CODES[code] ?? `Code ${code}`}`,
|
|
377
|
+
this.providerName,
|
|
378
|
+
{ cause: new Error(JSON.stringify(result)) }
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
async _refundPayment(orderId, amount, currency) {
|
|
382
|
+
const currencyNum = this._getCurrencyNum(currency);
|
|
383
|
+
const params = {
|
|
384
|
+
DS_MERCHANT_MERCHANTCODE: this.opts.merchantCode,
|
|
385
|
+
DS_MERCHANT_TERMINAL: this.opts.terminal,
|
|
386
|
+
DS_MERCHANT_ORDER: orderId,
|
|
387
|
+
DS_MERCHANT_AMOUNT: String(amount),
|
|
388
|
+
DS_MERCHANT_CURRENCY: String(currencyNum),
|
|
389
|
+
DS_MERCHANT_TRANSACTIONTYPE: "3"
|
|
390
|
+
// Refund
|
|
391
|
+
};
|
|
392
|
+
const base64Params = Buffer.from(JSON.stringify(params)).toString(
|
|
393
|
+
"base64"
|
|
394
|
+
);
|
|
395
|
+
const signature = this._sign(orderId, base64Params);
|
|
396
|
+
const { ok, value: result } = await this._client.post("/", {
|
|
397
|
+
body: JSON.stringify({
|
|
398
|
+
Ds_SignatureVersion: "HMAC_SHA256_V1",
|
|
399
|
+
Ds_MerchantParameters: base64Params,
|
|
400
|
+
Ds_Signature: signature
|
|
401
|
+
})
|
|
402
|
+
});
|
|
403
|
+
if (!ok || !result) {
|
|
404
|
+
throw new OperationFailedError(
|
|
405
|
+
"Redsys REST API error: refund failed",
|
|
406
|
+
this.providerName
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Handle Redsys webhook notifications
|
|
413
|
+
*
|
|
414
|
+
* Redsys sends POST to notificationUrl with:
|
|
415
|
+
* - Ds_MerchantParameters (Base64 encoded response)
|
|
416
|
+
* - Ds_Signature (HMAC signature)
|
|
417
|
+
* - Ds_SignatureVersion
|
|
418
|
+
*/
|
|
419
|
+
handleWebhook = async (payload, _webhookSecret) => {
|
|
420
|
+
const { body } = payload;
|
|
421
|
+
const dataAsObject = JSON.parse(body);
|
|
422
|
+
const paramsBase64 = dataAsObject.Ds_MerchantParameters;
|
|
423
|
+
if (!paramsBase64) {
|
|
424
|
+
throw new WebhookError("Missing Ds_MerchantParameters", {
|
|
425
|
+
provider: this.providerName
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const paramsJson = Buffer.from(paramsBase64, "base64").toString(
|
|
429
|
+
"utf-8"
|
|
430
|
+
);
|
|
431
|
+
const params = JSON.parse(paramsJson);
|
|
432
|
+
const dsResponse = String(params.Ds_Response ?? "");
|
|
433
|
+
const dsOrder = params.Ds_Order;
|
|
434
|
+
const dsAmount = params.Ds_Amount;
|
|
435
|
+
const dsMerchantData = params.Ds_MerchantData;
|
|
436
|
+
const signature = dataAsObject.Ds_Signature;
|
|
437
|
+
const expectedSig = this._sign(dsOrder, paramsBase64);
|
|
438
|
+
const sigOk = signature === expectedSig;
|
|
439
|
+
if (!sigOk) {
|
|
440
|
+
throw new WebhookError("Invalid Redsys webhook signature", {
|
|
441
|
+
provider: this.providerName
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
const events = [];
|
|
445
|
+
if (dsResponse === "0000" || dsResponse.startsWith("00")) {
|
|
446
|
+
const customerId = parseJSON(
|
|
447
|
+
Buffer.from(dsMerchantData, "base64").toString("utf-8"),
|
|
448
|
+
Schema.object({
|
|
449
|
+
customerId: Schema.string().optional()
|
|
450
|
+
})
|
|
451
|
+
)?.customerId ?? null;
|
|
452
|
+
const amountNum = Number(dsAmount) / 100;
|
|
453
|
+
const paymentId = `${dsOrder}_${params.Ds_AuthorisationCode ?? "unknown"}`;
|
|
454
|
+
events.push(
|
|
455
|
+
{
|
|
456
|
+
event: "redsys.payment.succeeded",
|
|
457
|
+
data: {
|
|
458
|
+
order_id: dsOrder,
|
|
459
|
+
amount: amountNum,
|
|
460
|
+
response_code: dsResponse,
|
|
461
|
+
customer_id: customerId
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
paykitEvent$InboundSchema({
|
|
465
|
+
type: "payment.succeeded",
|
|
466
|
+
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
467
|
+
id: randomBytes(8).toString("hex").slice(0, 15),
|
|
468
|
+
data: {
|
|
469
|
+
id: paymentId,
|
|
470
|
+
amount: amountNum,
|
|
471
|
+
currency: "EUR",
|
|
472
|
+
customer: customerId ? { id: customerId } : null,
|
|
473
|
+
status: "succeeded",
|
|
474
|
+
metadata: {},
|
|
475
|
+
item_id: null,
|
|
476
|
+
requires_action: false,
|
|
477
|
+
payment_url: null
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
);
|
|
481
|
+
} else {
|
|
482
|
+
events.push(
|
|
483
|
+
{
|
|
484
|
+
event: "redsys.payment.failed",
|
|
485
|
+
data: {
|
|
486
|
+
order_id: dsOrder,
|
|
487
|
+
response_code: dsResponse,
|
|
488
|
+
error_message: RESPONSE_CODES[dsResponse] ?? `Code ${dsResponse}`
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
paykitEvent$InboundSchema({
|
|
492
|
+
type: "payment.failed",
|
|
493
|
+
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
494
|
+
id: randomBytes(8).toString("hex").slice(0, 15),
|
|
495
|
+
data: {
|
|
496
|
+
id: dsOrder,
|
|
497
|
+
amount: Number(dsAmount) / 100,
|
|
498
|
+
currency: "EUR",
|
|
499
|
+
customer: null,
|
|
500
|
+
status: "failed",
|
|
501
|
+
metadata: {},
|
|
502
|
+
item_id: null,
|
|
503
|
+
requires_action: false,
|
|
504
|
+
payment_url: null
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
return events;
|
|
510
|
+
};
|
|
511
|
+
_notSupported(method) {
|
|
512
|
+
throw new NotImplementedError(
|
|
513
|
+
`Redsys doesn't support ${method}`,
|
|
514
|
+
this.providerName,
|
|
515
|
+
{ futureSupport: true }
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/index.ts
|
|
521
|
+
var createRedsys = (config) => new RedsysProvider(config);
|
|
522
|
+
var redsys = () => {
|
|
523
|
+
const envVars = validateRequiredKeys(
|
|
524
|
+
["REDSYS_MERCHANT_CODE", "REDSYS_TERMINAL", "REDSYS_SECRET_KEY"],
|
|
525
|
+
process.env ?? {},
|
|
526
|
+
"Missing required environment variables: {keys}"
|
|
527
|
+
);
|
|
528
|
+
const isSandbox = process.env.NODE_ENV !== "production";
|
|
529
|
+
return createRedsys({
|
|
530
|
+
merchantCode: envVars.REDSYS_MERCHANT_CODE,
|
|
531
|
+
terminal: envVars.REDSYS_TERMINAL,
|
|
532
|
+
secretKey: envVars.REDSYS_SECRET_KEY,
|
|
533
|
+
isSandbox,
|
|
534
|
+
debug: isSandbox,
|
|
535
|
+
transactionType: process.env.REDSYS_TRANSACTION_TYPE ?? "0"
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
export { RedsysProvider, createRedsys, redsys };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { AbstractPayKitProvider, PayKitProvider, ProviderMetadataRegistry, PaykitProviderOptions, CreateCheckoutSchema, Checkout, UpdateCheckoutSchema, CreatePaymentSchema, Payment, UpdatePaymentSchema, CapturePaymentSchema, CreateCustomerParams, Customer, UpdateCustomerParams, CreateSubscriptionSchema, UpdateSubscriptionSchema, CreateRefundSchema, Refund, WebhookHandlerConfig, WebhookEventPayload } from '@paykit-sdk/core';
|
|
2
|
+
|
|
3
|
+
interface RedsysOptions extends PaykitProviderOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Redsys merchant code (Ds_Merchant_MerchantCode)
|
|
6
|
+
*/
|
|
7
|
+
merchantCode: string;
|
|
8
|
+
/**
|
|
9
|
+
* Redsys terminal number (Ds_Merchant_Terminal)
|
|
10
|
+
*/
|
|
11
|
+
terminal: string;
|
|
12
|
+
/**
|
|
13
|
+
* Secret key for HMAC-SHA256 signing (from Redsys backend)
|
|
14
|
+
*/
|
|
15
|
+
secretKey: string;
|
|
16
|
+
/**
|
|
17
|
+
* Transaction type: "0" = immediate capture, "1" = pre-authorization
|
|
18
|
+
* @default "0"
|
|
19
|
+
*/
|
|
20
|
+
transactionType?: '0' | '1';
|
|
21
|
+
/**
|
|
22
|
+
* Sandbox: https://sis-t..redsys.es:25443/sis/realizarPago
|
|
23
|
+
* Production: https://sis.redsys.es/sis/realizarPago
|
|
24
|
+
*/
|
|
25
|
+
redsysUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Redsys inSite integration
|
|
29
|
+
*
|
|
30
|
+
* Flow:
|
|
31
|
+
* 1. createCheckout() → returns merchant parameters + signature for inSite iframe
|
|
32
|
+
* 2. Frontend loads redsysV3.js, renders inSite iframe with returned params
|
|
33
|
+
* 3. User enters card details in iframe → Redsys returns operationId
|
|
34
|
+
* 4. createPayment(operationId) → backend calls Redsys REST API to execute
|
|
35
|
+
* 5. handleWebhook() → processes server-side notifications from Redsys
|
|
36
|
+
*/
|
|
37
|
+
interface RedsysMetadata extends ProviderMetadataRegistry {
|
|
38
|
+
checkout: {
|
|
39
|
+
/** Currency numeric code (e.g., 978 for EUR) */
|
|
40
|
+
currency?: number;
|
|
41
|
+
/** Order ID generated by PayKit */
|
|
42
|
+
orderId?: string;
|
|
43
|
+
};
|
|
44
|
+
payment: {
|
|
45
|
+
/** Operation ID from inSite iframe */
|
|
46
|
+
operationId?: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
type RedsysRawEvents = {
|
|
50
|
+
[K in `redsys.${string}`]: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
declare class RedsysProvider extends AbstractPayKitProvider implements PayKitProvider<RedsysMetadata, never, RedsysRawEvents> {
|
|
53
|
+
private readonly opts;
|
|
54
|
+
private readonly _client;
|
|
55
|
+
readonly isSandbox: boolean;
|
|
56
|
+
constructor(opts: RedsysOptions);
|
|
57
|
+
readonly providerName = "redsys";
|
|
58
|
+
get _native(): never;
|
|
59
|
+
private _getRedsysUrl;
|
|
60
|
+
private _getCurrencyNum;
|
|
61
|
+
private _generateOrderId;
|
|
62
|
+
/**
|
|
63
|
+
* Build the Base64-encoded merchant parameters for inSite
|
|
64
|
+
*/
|
|
65
|
+
private _buildMerchantParams;
|
|
66
|
+
/**
|
|
67
|
+
* Generate HMAC-SHA256 signature for inSite
|
|
68
|
+
*
|
|
69
|
+
* Signature = HMAC-SHA256(secretKey, base64(params) + orderId)
|
|
70
|
+
* Order is important: base64Params + orderId (both as-is, no separators)
|
|
71
|
+
*/
|
|
72
|
+
private _sign;
|
|
73
|
+
/**
|
|
74
|
+
* Create checkout for inSite
|
|
75
|
+
*
|
|
76
|
+
* Returns merchant parameters and signature for the frontend iframe.
|
|
77
|
+
* The frontend will load redsysV3.js and render the inSite form.
|
|
78
|
+
*/
|
|
79
|
+
createCheckout: (params: CreateCheckoutSchema<RedsysMetadata["checkout"]>) => Promise<Checkout>;
|
|
80
|
+
retrieveCheckout: (id: string) => Promise<Checkout>;
|
|
81
|
+
updateCheckout: (id: string, _params: UpdateCheckoutSchema<RedsysMetadata["checkout"]>) => Promise<Checkout>;
|
|
82
|
+
deleteCheckout: (_id: string) => Promise<null>;
|
|
83
|
+
/**
|
|
84
|
+
* Create payment using operation ID from inSite
|
|
85
|
+
*
|
|
86
|
+
* The operation ID is returned by the inSite iframe when the user
|
|
87
|
+
* completes the card entry. The frontend should pass it in
|
|
88
|
+
* provider_metadata.operationId.
|
|
89
|
+
*/
|
|
90
|
+
createPayment: (params: CreatePaymentSchema<RedsysMetadata["payment"]>) => Promise<Payment>;
|
|
91
|
+
/**
|
|
92
|
+
* Execute payment via Redsys REST API
|
|
93
|
+
*/
|
|
94
|
+
private _executePayment;
|
|
95
|
+
retrievePayment: (id: string) => Promise<Payment | null>;
|
|
96
|
+
updatePayment: (id: string, _params: UpdatePaymentSchema<RedsysMetadata["payment"]>) => Promise<Payment>;
|
|
97
|
+
deletePayment: (_id: string) => Promise<null>;
|
|
98
|
+
capturePayment: (id: string, _params: CapturePaymentSchema) => Promise<Payment>;
|
|
99
|
+
cancelPayment: (id: string) => Promise<Payment>;
|
|
100
|
+
createCustomer: (params: CreateCustomerParams<RedsysMetadata["customer"]>) => Promise<Customer>;
|
|
101
|
+
retrieveCustomer: (_id: string) => Promise<Customer | null>;
|
|
102
|
+
updateCustomer: (id: string, _params: UpdateCustomerParams<RedsysMetadata["customer"]>) => Promise<Customer>;
|
|
103
|
+
deleteCustomer: (_id: string) => Promise<null>;
|
|
104
|
+
createSubscription: (_params: CreateSubscriptionSchema<RedsysMetadata["subscription"]>) => Promise<never>;
|
|
105
|
+
retrieveSubscription: (_id: string) => Promise<never>;
|
|
106
|
+
updateSubscription: (_id: string, _params: UpdateSubscriptionSchema<RedsysMetadata["subscription"]>) => Promise<never>;
|
|
107
|
+
deleteSubscription: (_id: string) => Promise<null>;
|
|
108
|
+
cancelSubscription: (_id: string) => Promise<never>;
|
|
109
|
+
createRefund: (params: CreateRefundSchema<RedsysMetadata["refund"]>) => Promise<Refund>;
|
|
110
|
+
private _refundPayment;
|
|
111
|
+
/**
|
|
112
|
+
* Handle Redsys webhook notifications
|
|
113
|
+
*
|
|
114
|
+
* Redsys sends POST to notificationUrl with:
|
|
115
|
+
* - Ds_MerchantParameters (Base64 encoded response)
|
|
116
|
+
* - Ds_Signature (HMAC signature)
|
|
117
|
+
* - Ds_SignatureVersion
|
|
118
|
+
*/
|
|
119
|
+
handleWebhook: (payload: WebhookHandlerConfig, _webhookSecret: string) => Promise<Array<WebhookEventPayload<RedsysRawEvents>>>;
|
|
120
|
+
private _notSupported;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { type RedsysOptions, RedsysProvider };
|