redsys-easy 3.1.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -419
- package/lib/assets/card-brands.d.ts +26 -2
- package/lib/assets/countries.d.ts +29 -2
- package/lib/assets/currencies.d.ts +26 -2
- package/lib/assets/lang-codes.d.ts +29 -2
- package/lib/assets/transaction-types.d.ts +38 -3
- package/lib/client.d.ts +50 -0
- package/lib/crypto.d.ts +25 -0
- package/lib/errors.d.ts +61 -6
- package/lib/formatter-utils.d.ts +24 -0
- package/lib/formatters/input-params.d.ts +21 -0
- package/lib/formatters/output-params.d.ts +29 -0
- package/lib/formatters/types.d.ts +316 -0
- package/lib/index.d.ts +30 -56
- package/lib/index.esm.js +957 -746
- package/lib/index.js +990 -747
- package/lib/rest/3ds.d.ts +31 -0
- package/lib/rest/json-serialization.d.ts +2 -0
- package/lib/rest/json-sha256-signature.d.ts +3 -0
- package/lib/rest/json.d.ts +3 -0
- package/lib/rest/requests.d.ts +2 -0
- package/lib/soap/notification-serialization.d.ts +3 -0
- package/lib/soap/notification-signature.d.ts +3 -0
- package/lib/soap/notification.d.ts +4 -0
- package/lib/soap/utils.d.ts +43 -0
- package/lib/types/3ds-params.d.ts +345 -0
- package/lib/types/api.d.ts +69 -78
- package/lib/types/input-params.d.ts +176 -0
- package/lib/types/output-params.d.ts +158 -0
- package/lib/utils/codes.d.ts +13 -0
- package/lib/utils/misc.d.ts +24 -0
- package/package.json +63 -57
- package/CHANGELOG.md +0 -53
- package/lib/params-formatter.d.ts +0 -7
- package/lib/response-formatter.d.ts +0 -5
- package/lib/utils.d.ts +0 -39
package/lib/index.esm.js
CHANGED
|
@@ -1,42 +1,13 @@
|
|
|
1
|
+
import { STATUS_CODES } from 'http';
|
|
1
2
|
import base64url from 'base64url';
|
|
2
|
-
import xmlParser, { j2xParser } from 'fast-xml-parser';
|
|
3
|
-
import { createClientAsync } from 'soap';
|
|
4
3
|
import crypto from 'crypto';
|
|
4
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import Decimal from 'decimal.js';
|
|
5
7
|
import dayjs from 'dayjs';
|
|
6
8
|
import timezone from 'dayjs/plugin/timezone';
|
|
7
9
|
import utc from 'dayjs/plugin/utc';
|
|
8
10
|
|
|
9
|
-
class RedsysError extends Error {
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'RedsysError';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
class ValidationError extends RedsysError {
|
|
16
|
-
constructor(message, value, field) {
|
|
17
|
-
super(message);
|
|
18
|
-
this.name = 'RedsysValidationError';
|
|
19
|
-
this.value = value;
|
|
20
|
-
this.field = field;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
class ParseError extends RedsysError {
|
|
24
|
-
constructor(message, value, text) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.name = 'RedsysParseError';
|
|
27
|
-
this.value = value;
|
|
28
|
-
this.text = text;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
class GatewayError extends RedsysError {
|
|
32
|
-
constructor(message, code, response) {
|
|
33
|
-
super(message);
|
|
34
|
-
this.name = 'RedsysParseError';
|
|
35
|
-
this.code = code;
|
|
36
|
-
this.response = response;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
11
|
const ALL_ERRORS = [
|
|
41
12
|
{ code: 9001, sisCode: 'SIS0001', text: 'Error Interno' },
|
|
42
13
|
{ code: 9002, sisCode: 'SIS0002', text: 'Error genérico' },
|
|
@@ -490,24 +461,151 @@ const RESPONSE_CODES = {
|
|
|
490
461
|
900: 'Transacción autorizada para devoluciones y confirmaciones.'
|
|
491
462
|
};
|
|
492
463
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Returns the message corresponding to a response code, in spanish
|
|
466
|
+
*
|
|
467
|
+
* @public
|
|
468
|
+
*/
|
|
469
|
+
const getResponseCodeMessage = (code) => {
|
|
470
|
+
if (typeof code !== 'number' && typeof code !== 'string') {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const lookupNum = typeof code === 'string' ? Number.parseFloat(code.trim()) : code;
|
|
474
|
+
if (!Number.isInteger(lookupNum) || lookupNum < 0) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const msg = RESPONSE_CODES[lookupNum.toString()];
|
|
478
|
+
if ((msg == null || !msg) && lookupNum < 100) {
|
|
479
|
+
return 'Transacción autorizada para pagos y preautorizaciones';
|
|
480
|
+
}
|
|
481
|
+
return msg;
|
|
482
|
+
};
|
|
483
|
+
/**
|
|
484
|
+
* Returns the message corresponding to a gateway error code
|
|
485
|
+
*
|
|
486
|
+
* @public
|
|
487
|
+
*/
|
|
488
|
+
const getSISErrorCodeMessage = (code) => {
|
|
489
|
+
if (!code || typeof code !== 'string') {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
return SIS_ERROR_CODES[code.trim()];
|
|
493
|
+
};
|
|
494
|
+
const getHTTPErrorCodeMessage = (code) => {
|
|
495
|
+
return STATUS_CODES[code];
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Error superclass for all redsys-easy specific errors
|
|
500
|
+
*
|
|
501
|
+
* @public
|
|
502
|
+
*/
|
|
503
|
+
class RedsysError extends Error {
|
|
504
|
+
constructor(message) {
|
|
505
|
+
super(message);
|
|
506
|
+
this.name = 'RedsysError';
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Invalid input provided
|
|
511
|
+
*
|
|
512
|
+
* @public
|
|
513
|
+
*/
|
|
514
|
+
class ValidationError extends RedsysError {
|
|
515
|
+
constructor(message, parameters) {
|
|
516
|
+
super(message);
|
|
517
|
+
this.name = 'RedsysValidationError';
|
|
518
|
+
this.parameters = parameters;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Response cannot be parsed
|
|
523
|
+
*
|
|
524
|
+
* @public
|
|
525
|
+
*/
|
|
526
|
+
class ParseError extends RedsysError {
|
|
527
|
+
constructor(message, value, location) {
|
|
528
|
+
super(message);
|
|
529
|
+
this.name = 'RedsysParseError';
|
|
530
|
+
this.value = value;
|
|
531
|
+
this.location = location;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* HTTP Error
|
|
536
|
+
*
|
|
537
|
+
* @public
|
|
538
|
+
*/
|
|
539
|
+
class HTTPError extends RedsysError {
|
|
540
|
+
constructor({ message, code, response }) {
|
|
541
|
+
const codeDescription = getHTTPErrorCodeMessage(code);
|
|
542
|
+
const codeInfo = code !== undefined
|
|
543
|
+
? ` ${code}${codeDescription != null ? `: ${codeDescription}` : ''}`
|
|
544
|
+
: '';
|
|
545
|
+
const augmentationLine = message != null && message ? `\n${message}` : '';
|
|
546
|
+
const augmentedMessage = `HTTP error${codeInfo}${augmentationLine}`;
|
|
547
|
+
super(augmentedMessage);
|
|
548
|
+
this.name = 'RedsysHTTPError';
|
|
549
|
+
this.code = code;
|
|
550
|
+
this.response = response;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Request could not be processed by Redsys
|
|
555
|
+
*
|
|
556
|
+
* @public
|
|
557
|
+
*/
|
|
558
|
+
class GatewayError extends RedsysError {
|
|
559
|
+
constructor({ message, code, response }) {
|
|
560
|
+
const codeDescription = getSISErrorCodeMessage(code);
|
|
561
|
+
const codeInfo = code !== undefined
|
|
562
|
+
? ` ${code}${codeDescription != null ? `: ${codeDescription}` : ''}`
|
|
563
|
+
: '';
|
|
564
|
+
const augmentationLine = message != null && message ? `\n${message}` : '';
|
|
565
|
+
const augmentedMessage = `Gateway error${codeInfo}${augmentationLine}`;
|
|
566
|
+
super(augmentedMessage);
|
|
567
|
+
this.name = 'RedsysGatewayError';
|
|
568
|
+
this.code = code;
|
|
569
|
+
this.response = response;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Response contained an error code
|
|
574
|
+
*
|
|
575
|
+
* @public
|
|
576
|
+
*/
|
|
577
|
+
class ResponseError extends RedsysError {
|
|
578
|
+
constructor({ message, code, response }) {
|
|
579
|
+
const codeDescription = getResponseCodeMessage(code);
|
|
580
|
+
const codeInfo = code !== undefined
|
|
581
|
+
? ` ${code}${codeDescription != null ? `: ${codeDescription}` : ''}`
|
|
582
|
+
: '';
|
|
583
|
+
const augmentationLine = message != null && message ? `\n${message}` : '';
|
|
584
|
+
const augmentedMessage = `Response error${codeInfo}${augmentationLine}`;
|
|
585
|
+
super(augmentedMessage);
|
|
586
|
+
this.name = 'RedsysResponseError';
|
|
587
|
+
this.code = code;
|
|
588
|
+
this.response = response;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const deserializeJSONMerchantParams = (strPayload) => {
|
|
593
|
+
if (!strPayload) {
|
|
594
|
+
throw new ParseError('Payload is empty');
|
|
595
|
+
}
|
|
596
|
+
if (typeof strPayload !== 'string') {
|
|
597
|
+
throw new ParseError('Payload must be a base-64 encoded string');
|
|
598
|
+
}
|
|
599
|
+
const payload = JSON.parse(decodeURIComponent(Buffer.from(strPayload, 'base64').toString('utf8')));
|
|
600
|
+
if (typeof payload !== 'object' || payload == null) {
|
|
601
|
+
throw new ParseError('Cannot parse notification payload');
|
|
602
|
+
}
|
|
603
|
+
return payload;
|
|
604
|
+
};
|
|
605
|
+
const serializeJSONMerchantParams = (requestParams) => {
|
|
606
|
+
// Official redsys docs escape "/" because they use php json_encode. I think
|
|
607
|
+
// this is wrong and it also works without escaping.
|
|
608
|
+
return Buffer.from(JSON.stringify(requestParams), 'utf8').toString('base64');
|
|
511
609
|
};
|
|
512
610
|
|
|
513
611
|
/**
|
|
@@ -515,9 +613,8 @@ const TRANSACTION_TYPES = {
|
|
|
515
613
|
*
|
|
516
614
|
* Rounds up the buffer length to the next block and uses 0 as padding.
|
|
517
615
|
*
|
|
518
|
-
* @params
|
|
519
|
-
* @params
|
|
520
|
-
* @return {buffer}
|
|
616
|
+
* @params buf - Input buffer
|
|
617
|
+
* @params blocksize - Size of block
|
|
521
618
|
*/
|
|
522
619
|
const zeroPad = (buf, blocksize) => {
|
|
523
620
|
const pad = Buffer.alloc((blocksize - (buf.length % blocksize)) % blocksize, 0);
|
|
@@ -526,9 +623,8 @@ const zeroPad = (buf, blocksize) => {
|
|
|
526
623
|
/**
|
|
527
624
|
* Encrypt a message using 3DES
|
|
528
625
|
*
|
|
529
|
-
* @params
|
|
530
|
-
* @params
|
|
531
|
-
* @return {buffer}
|
|
626
|
+
* @params key - Key to encrypt message
|
|
627
|
+
* @params message - Message to be encrypted
|
|
532
628
|
*/
|
|
533
629
|
const encrypt3DES = (key, message) => {
|
|
534
630
|
const keyBuf = Buffer.from(key, 'base64');
|
|
@@ -543,34 +639,353 @@ const encrypt3DES = (key, message) => {
|
|
|
543
639
|
const maxLength = Math.ceil(messageBuf.length / 8) * 8;
|
|
544
640
|
return encryptedBuf.slice(0, maxLength);
|
|
545
641
|
};
|
|
642
|
+
/**
|
|
643
|
+
* Compute HMAC_SHA256_V1 signature
|
|
644
|
+
*
|
|
645
|
+
* @params merchantKey - Key to encrypt message
|
|
646
|
+
* @params order - Order number
|
|
647
|
+
* @params params - Payload to sign
|
|
648
|
+
*/
|
|
546
649
|
const sha256Sign = (merchantKey, order, params) => {
|
|
547
650
|
const orderKeyBuf = encrypt3DES(merchantKey, order);
|
|
548
651
|
return crypto.createHmac('sha256', orderKeyBuf).update(params).digest('base64');
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Discrete uniform distribution with domain (0 to *max*)
|
|
656
|
+
*/
|
|
657
|
+
const drawPositiveDiscreteUniform = (max) => {
|
|
658
|
+
return Math.floor(Math.random() * (max + 1));
|
|
549
659
|
};
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
660
|
+
const alphanumeric = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
661
|
+
/**
|
|
662
|
+
* Generates a random order ID following redsys requirements
|
|
663
|
+
*
|
|
664
|
+
* @public
|
|
665
|
+
*/
|
|
666
|
+
const randomTransactionId = () => {
|
|
667
|
+
// Random 4 digit number between 0 and 9999
|
|
668
|
+
const num = drawPositiveDiscreteUniform(9999);
|
|
669
|
+
const numStr = num.toString().padStart(4, '0');
|
|
670
|
+
// Random 8 letter alphanumeric sequence
|
|
671
|
+
const alphanum = new Array(8);
|
|
672
|
+
for (let idx = 0; idx < 8; idx++) {
|
|
673
|
+
alphanum[idx] = alphanumeric[drawPositiveDiscreteUniform(alphanumeric.length - 1)];
|
|
553
674
|
}
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
675
|
+
const alphanumStr = alphanum.join('');
|
|
676
|
+
return `${numStr}${alphanumStr}`;
|
|
677
|
+
};
|
|
678
|
+
const extractAndAssertOrderFromRequestParams = (requestParams) => {
|
|
679
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing
|
|
680
|
+
const order = requestParams['DS_MERCHANT_ORDER'] || requestParams['Ds_Merchant_Order'];
|
|
681
|
+
if (typeof order !== 'string' || !order) {
|
|
682
|
+
throw new ParseError('Missing order number');
|
|
557
683
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
684
|
+
return order;
|
|
685
|
+
};
|
|
686
|
+
const extractAndAssertOrderFromResponseParams = (responseParams) => {
|
|
687
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing
|
|
688
|
+
const order = responseParams['Ds_Order'] || responseParams['DS_ORDER'];
|
|
689
|
+
if (typeof order !== 'string' || !order) {
|
|
690
|
+
throw new ParseError('Missing order number');
|
|
561
691
|
}
|
|
562
|
-
return
|
|
692
|
+
return order;
|
|
563
693
|
};
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
694
|
+
/**
|
|
695
|
+
* Indicates if a response code signals success
|
|
696
|
+
*
|
|
697
|
+
* @public
|
|
698
|
+
*/
|
|
699
|
+
const isResponseCodeOk = (responseCode) => {
|
|
700
|
+
const numResCode = Number.parseInt(responseCode);
|
|
701
|
+
return Number.isFinite(numResCode) && ((numResCode >= 0 && numResCode < 100) ||
|
|
702
|
+
numResCode === 400 || // Transaction cancellation authorized
|
|
703
|
+
numResCode === 900 // Refund authorized
|
|
704
|
+
);
|
|
705
|
+
};
|
|
706
|
+
/**
|
|
707
|
+
* Asserts that response has a code that indicates success
|
|
708
|
+
*
|
|
709
|
+
* @public
|
|
710
|
+
*/
|
|
711
|
+
const assertSuccessfulResponseCode = (responseParams) => {
|
|
712
|
+
const { Ds_Response: resCode } = responseParams;
|
|
713
|
+
if (resCode == null) {
|
|
714
|
+
throw new ParseError('Missing response code');
|
|
567
715
|
}
|
|
568
|
-
|
|
716
|
+
if (!isResponseCodeOk(resCode)) {
|
|
717
|
+
throw new ResponseError({ code: Number.parseInt(resCode), response: responseParams });
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
const isStringNotEmpty = (str) => str != null && str.length > 0;
|
|
721
|
+
|
|
722
|
+
const sha256VerifyJSONResponse = (merchantKey, response, responseParams) => {
|
|
723
|
+
if (response.Ds_SignatureVersion !== 'HMAC_SHA256_V1') {
|
|
724
|
+
throw new RedsysError(`Unknown signature version: ${response.Ds_SignatureVersion}`);
|
|
725
|
+
}
|
|
726
|
+
const order = extractAndAssertOrderFromResponseParams(responseParams);
|
|
727
|
+
const signature = response.Ds_Signature;
|
|
728
|
+
// Base64url with padding, only substitutes + and /
|
|
729
|
+
const expSignature = sha256Sign(merchantKey, order, response.Ds_MerchantParameters);
|
|
730
|
+
// Comparing different base64 encodings is messy. Make it foolproof by comparing buffers.
|
|
731
|
+
if (!signature || !Buffer.from(expSignature, 'base64').equals(base64url.toBuffer(signature))) {
|
|
732
|
+
throw new ParseError('Invalid signature');
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const sha256SignJSONRequest = (merchantKey, serializedParams, requestParams) => {
|
|
736
|
+
const order = extractAndAssertOrderFromRequestParams(requestParams);
|
|
737
|
+
const signature = sha256Sign(merchantKey, order, serializedParams);
|
|
738
|
+
return {
|
|
739
|
+
Ds_SignatureVersion: 'HMAC_SHA256_V1',
|
|
740
|
+
Ds_MerchantParameters: serializedParams,
|
|
741
|
+
Ds_Signature: signature
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const deserializeAndVerifyJSONResponse = (merchantKey, response) => {
|
|
746
|
+
const params = deserializeJSONMerchantParams(response.Ds_MerchantParameters);
|
|
747
|
+
sha256VerifyJSONResponse(merchantKey, response, params);
|
|
748
|
+
return params;
|
|
749
|
+
};
|
|
750
|
+
const serializeAndSignJSONRequest = (merchantKey, requestParams) => {
|
|
751
|
+
const serializedParams = serializeJSONMerchantParams(requestParams);
|
|
752
|
+
return sha256SignJSONRequest(merchantKey, serializedParams, requestParams);
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const serializeSoapNotificationResponse = ({ allow: allowOperation }) => {
|
|
756
|
+
const answer = allowOperation ? 'OK' : 'KO';
|
|
757
|
+
return `<Response Ds_Version="0.0"><Ds_Response_Merchant>${answer}</Ds_Response_Merchant></Response>`;
|
|
758
|
+
};
|
|
759
|
+
const xmlParser = new XMLParser({ parseTagValue: false });
|
|
760
|
+
const deserializeSoapNotification = (xml) => {
|
|
761
|
+
return xmlParser.parse(xml).Message;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const verifySoapNotification = (merchantKey, xml, msg) => {
|
|
765
|
+
const startToken = '<Request';
|
|
766
|
+
const endToken = '</Request>';
|
|
767
|
+
const startPos = xml.indexOf(startToken);
|
|
768
|
+
const endPos = xml.indexOf(endToken);
|
|
769
|
+
if (startPos < 0 || endPos < 0 || startPos > endPos) {
|
|
770
|
+
throw new ParseError('Cannot find payload of SOAP notification Request', xml);
|
|
771
|
+
}
|
|
772
|
+
const signedStr = xml.slice(startPos, endPos + endToken.length);
|
|
773
|
+
const signature = msg.Signature;
|
|
774
|
+
const order = msg.Request.Ds_Order;
|
|
775
|
+
if (order == null || !order) {
|
|
776
|
+
throw new ParseError('Invalid notification response');
|
|
777
|
+
}
|
|
778
|
+
const expSignature = sha256Sign(merchantKey, order, signedStr);
|
|
779
|
+
if (!signature || signature !== expSignature) {
|
|
780
|
+
throw new ParseError('Invalid signature', signature, xml);
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
const signSoapNotificationResponse = (merchantKey, serializedResponse, response) => {
|
|
784
|
+
const signature = sha256Sign(merchantKey, response.order, serializedResponse);
|
|
785
|
+
return `<Message>${serializedResponse}<Signature>${signature}</Signature></Message>`;
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const serializeAndSignSoapNotificationResponse = (merchantKey, response) => {
|
|
789
|
+
const serializedResponse = serializeSoapNotificationResponse(response);
|
|
790
|
+
return signSoapNotificationResponse(merchantKey, serializedResponse, response);
|
|
791
|
+
};
|
|
792
|
+
const deserializeAndVerifySoapNotification = (merchantKey, xml) => {
|
|
793
|
+
const msg = deserializeSoapNotification(xml);
|
|
794
|
+
verifySoapNotification(merchantKey, xml, msg);
|
|
795
|
+
// Return only parameters. Signature is useless after verification
|
|
796
|
+
return msg.Request;
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const jsonRequest = async (url, merchantKey, rawRequestParams) => {
|
|
800
|
+
const payload = serializeAndSignJSONRequest(merchantKey, rawRequestParams);
|
|
801
|
+
const response = await fetch(url, {
|
|
802
|
+
method: 'POST',
|
|
803
|
+
headers: {
|
|
804
|
+
'Content-Type': 'application/json'
|
|
805
|
+
},
|
|
806
|
+
body: JSON.stringify(payload)
|
|
807
|
+
});
|
|
808
|
+
const responseData = await response.json();
|
|
809
|
+
if (!response.ok) {
|
|
810
|
+
throw new HTTPError({
|
|
811
|
+
code: response.status,
|
|
812
|
+
response: responseData
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
if ('errorCode' in responseData) {
|
|
816
|
+
throw new GatewayError({
|
|
817
|
+
code: responseData.errorCode,
|
|
818
|
+
response: responseData
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
return deserializeAndVerifyJSONResponse(merchantKey, responseData);
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Redsys production urls
|
|
826
|
+
*
|
|
827
|
+
* @public
|
|
828
|
+
*/
|
|
829
|
+
const PRODUCTION_URLS = {
|
|
830
|
+
redirect: 'https://sis.redsys.es/sis/realizarPago',
|
|
831
|
+
restTrataPeticion: 'https://sis.redsys.es/sis/rest/trataPeticionREST',
|
|
832
|
+
restIniciaPeticion: 'https://sis.redsys.es/sis/rest/iniciaPeticionREST'
|
|
833
|
+
};
|
|
834
|
+
/**
|
|
835
|
+
* Redsys sandbox urls
|
|
836
|
+
*
|
|
837
|
+
* @public
|
|
838
|
+
*/
|
|
839
|
+
const SANDBOX_URLS = {
|
|
840
|
+
redirect: 'https://sis-t.redsys.es:25443/sis/realizarPago',
|
|
841
|
+
restTrataPeticion: 'https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST',
|
|
842
|
+
restIniciaPeticion: 'https://sis-t.redsys.es:25443/sis/rest/iniciaPeticionREST'
|
|
843
|
+
};
|
|
844
|
+
/**
|
|
845
|
+
* Creates Redsys API functions
|
|
846
|
+
*
|
|
847
|
+
* @remarks
|
|
848
|
+
* These API functions are anonymous and can be wrapped by formatters and processors
|
|
849
|
+
*
|
|
850
|
+
* @public
|
|
851
|
+
*/
|
|
852
|
+
const createRedsysAPI = (config) => {
|
|
853
|
+
if (!config.secretKey || typeof config.secretKey !== 'string') {
|
|
854
|
+
throw new RedsysError('A secretKey key must be provided');
|
|
855
|
+
}
|
|
856
|
+
if (typeof config.urls !== 'object' || config.urls == null ||
|
|
857
|
+
!config.urls.restIniciaPeticion || !config.urls.restTrataPeticion ||
|
|
858
|
+
!config.urls.redirect) {
|
|
859
|
+
throw new RedsysError('URLs must be provided');
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Sends a iniciaPeticion request using REST interface
|
|
863
|
+
*/
|
|
864
|
+
const restIniciaPeticion = async (paramsInput) => {
|
|
865
|
+
return await jsonRequest(config.urls.restIniciaPeticion, config.secretKey, paramsInput);
|
|
866
|
+
};
|
|
867
|
+
/**
|
|
868
|
+
* Sends a trataPeticion request using REST interface
|
|
869
|
+
*/
|
|
870
|
+
const restTrataPeticion = async (paramsInput) => {
|
|
871
|
+
const result = await jsonRequest(config.urls.restTrataPeticion, config.secretKey, paramsInput);
|
|
872
|
+
return result;
|
|
873
|
+
};
|
|
874
|
+
/**
|
|
875
|
+
* Creates the parameters needed for a redirect form
|
|
876
|
+
*/
|
|
877
|
+
const createRedirectForm = (paramsInput) => {
|
|
878
|
+
const body = serializeAndSignJSONRequest(config.secretKey, paramsInput);
|
|
879
|
+
return {
|
|
880
|
+
url: config.urls.redirect,
|
|
881
|
+
body
|
|
882
|
+
};
|
|
883
|
+
};
|
|
884
|
+
/**
|
|
885
|
+
* Processes a JSON REST notification
|
|
886
|
+
*/
|
|
887
|
+
const processRestNotification = (
|
|
888
|
+
/** Body of JSON notification, as a POJO (Plain Old Javascript Object) */
|
|
889
|
+
body) => {
|
|
890
|
+
// A notification can't contain a gateway error, it didn't initiate the request
|
|
891
|
+
return deserializeAndVerifyJSONResponse(config.secretKey, body);
|
|
892
|
+
};
|
|
893
|
+
/**
|
|
894
|
+
* Parses and verifies the body of a SOAP notification
|
|
895
|
+
*/
|
|
896
|
+
const processSoapNotification = (
|
|
897
|
+
/** SOAP notification as a XML string */
|
|
898
|
+
xml) => {
|
|
899
|
+
return deserializeAndVerifySoapNotification(config.secretKey, xml);
|
|
900
|
+
};
|
|
901
|
+
/**
|
|
902
|
+
* Creates an answer for a SOAP notification, serializes and signs it
|
|
903
|
+
*/
|
|
904
|
+
const createSoapNotificationAnswer = (
|
|
905
|
+
/** Order identifier */
|
|
906
|
+
order,
|
|
907
|
+
/** Indicates if the payment is allowed to proceed */
|
|
908
|
+
allow) => {
|
|
909
|
+
return serializeAndSignSoapNotificationResponse(config.secretKey, {
|
|
910
|
+
order,
|
|
911
|
+
allow
|
|
912
|
+
});
|
|
913
|
+
};
|
|
914
|
+
return {
|
|
915
|
+
restIniciaPeticion,
|
|
916
|
+
restTrataPeticion,
|
|
917
|
+
createRedirectForm,
|
|
918
|
+
processRestNotification,
|
|
919
|
+
processSoapNotification,
|
|
920
|
+
createSoapNotificationAnswer
|
|
921
|
+
};
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
/*
|
|
925
|
+
* 3DS method form
|
|
926
|
+
*/
|
|
927
|
+
/**
|
|
928
|
+
* Creates parameters for a 3DS method form
|
|
929
|
+
*
|
|
930
|
+
* @public
|
|
931
|
+
*/
|
|
932
|
+
const create3DSMethodForm = (emv3dsParams, notificationURL) => {
|
|
933
|
+
const methodData = {
|
|
934
|
+
threeDSServerTransID: emv3dsParams.threeDSServerTransID,
|
|
935
|
+
threeDSMethodNotificationURL: notificationURL
|
|
936
|
+
};
|
|
937
|
+
return {
|
|
938
|
+
url: emv3dsParams.threeDSMethodURL,
|
|
939
|
+
body: {
|
|
940
|
+
threeDSMethodData: base64url.encode(JSON.stringify(methodData))
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
/**
|
|
945
|
+
* Creates parameters for a 3DS v1 challenge form
|
|
946
|
+
*
|
|
947
|
+
* @public
|
|
948
|
+
*/
|
|
949
|
+
const create3DSv1ChallengeForm = (emv3dsV1Challenge, challengeNotificationUrl) => {
|
|
950
|
+
return {
|
|
951
|
+
url: emv3dsV1Challenge.acsURL,
|
|
952
|
+
body: {
|
|
953
|
+
PaReq: emv3dsV1Challenge.PAReq,
|
|
954
|
+
MD: emv3dsV1Challenge.MD,
|
|
955
|
+
TermUrl: challengeNotificationUrl
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
};
|
|
959
|
+
/**
|
|
960
|
+
* Creates parameters for a 3DS v2 challenge form
|
|
961
|
+
*
|
|
962
|
+
* @public
|
|
963
|
+
*/
|
|
964
|
+
const create3DSv2ChallengeForm = (emv3dsV2Challenge) => {
|
|
965
|
+
return {
|
|
966
|
+
url: emv3dsV2Challenge.acsURL,
|
|
967
|
+
body: {
|
|
968
|
+
creq: emv3dsV2Challenge.creq
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
};
|
|
972
|
+
/**
|
|
973
|
+
* Deserialize threeDSMethodData
|
|
974
|
+
*
|
|
975
|
+
* @public
|
|
976
|
+
*/
|
|
977
|
+
const deserializeThreeDSMethodData = (threeDSMethodData) => {
|
|
978
|
+
return JSON.parse(base64url.decode(threeDSMethodData));
|
|
569
979
|
};
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
980
|
+
/**
|
|
981
|
+
* Deserialize `cres` field of a 3DS v2 challenge
|
|
982
|
+
*
|
|
983
|
+
* @public
|
|
984
|
+
*/
|
|
985
|
+
const deserializeCres = (cres) => {
|
|
986
|
+
return JSON.parse(Buffer.from(cres, 'base64').toString('utf8'));
|
|
987
|
+
};
|
|
988
|
+
|
|
574
989
|
const unescapeXML = (str) => {
|
|
575
990
|
const xml = str.replace(/&(lt|#60|gt|#62|quot|#34|amp|#38|apos|#39);/g, (_match, p1) => {
|
|
576
991
|
switch (p1) {
|
|
@@ -614,6 +1029,11 @@ const escapeXML = (str) => {
|
|
|
614
1029
|
});
|
|
615
1030
|
return xml;
|
|
616
1031
|
};
|
|
1032
|
+
/**
|
|
1033
|
+
* Detects the soap version of a HTTP request
|
|
1034
|
+
*
|
|
1035
|
+
* @public
|
|
1036
|
+
*/
|
|
617
1037
|
const detectSoapVersion = (req) => {
|
|
618
1038
|
var _a;
|
|
619
1039
|
const contentType = (_a = req.headers) === null || _a === void 0 ? void 0 : _a['Content-Type'];
|
|
@@ -635,6 +1055,14 @@ const detectSoapVersion = (req) => {
|
|
|
635
1055
|
}
|
|
636
1056
|
throw new ParseError('Not a valid SOAP request', req);
|
|
637
1057
|
};
|
|
1058
|
+
/**
|
|
1059
|
+
* Extracts the body of a SOAP notification from the body of the HTTP request
|
|
1060
|
+
*
|
|
1061
|
+
* @remarks
|
|
1062
|
+
* It does not understand SOAP or use WSDL, the implementation is a little bit hacky
|
|
1063
|
+
*
|
|
1064
|
+
* @public
|
|
1065
|
+
*/
|
|
638
1066
|
const mimicSoapNotificationReceiver = (xml) => {
|
|
639
1067
|
const regex = /<(?:\w+:)?XML(?: [^<>]+)?>([^<>]+)<\/(?:\w+:)?XML>/;
|
|
640
1068
|
const matchArray = regex.exec(xml);
|
|
@@ -645,73 +1073,37 @@ const mimicSoapNotificationReceiver = (xml) => {
|
|
|
645
1073
|
// wsdl defines XML tag as a string. It cannot be a CDATA (which is a XML element).
|
|
646
1074
|
return unescapeXML(xmlContent);
|
|
647
1075
|
};
|
|
1076
|
+
/**
|
|
1077
|
+
* Creates the body of a HTTP response as SOAP 1.1 from the body of a SOAP notification response
|
|
1078
|
+
*
|
|
1079
|
+
* @remarks
|
|
1080
|
+
* It does not understand SOAP or use WSDL, the implementation is a little bit hacky
|
|
1081
|
+
*
|
|
1082
|
+
* @public
|
|
1083
|
+
*/
|
|
648
1084
|
const mimicSoap11NotificationResponse = (answer) => {
|
|
649
1085
|
const escapedAnswer = escapeXML(answer);
|
|
650
1086
|
return `<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="https://sis.sermepa.es/sis/InotificacionSIS.wsdl" xmlns:types="https://sis.sermepa.es/sis/InotificacionSIS.wsdl/encodedTypes" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><q2:procesaNotificacionSISResponse xmlns:q2="InotificacionSIS"><return xsi:type="xsd:string">${escapedAnswer}</return></q2:procesaNotificacionSISResponse></soap:Body></soap:Envelope>`;
|
|
651
1087
|
};
|
|
652
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* Creates the body of a HTTP response as SOAP 1.2 from the body of a SOAP notification response
|
|
1090
|
+
*
|
|
1091
|
+
* @remarks
|
|
1092
|
+
* It does not understand SOAP or use WSDL, the implementation is a little bit hacky
|
|
1093
|
+
*
|
|
1094
|
+
* @public
|
|
1095
|
+
*/
|
|
1096
|
+
const mimicSoap12NotificationResponse = (answer) => {
|
|
653
1097
|
const escapedAnswer = escapeXML(answer);
|
|
654
1098
|
return `<?xml version="1.0" encoding="utf-8"?><soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenc="http://www.w3.org/2003/05/soap-encoding" xmlns:tns="https://sis.sermepa.es/sis/InotificacionSIS.wsdl" xmlns:types="https://sis.sermepa.es/sis/InotificacionSIS.wsdl/encodedTypes" xmlns:rpc="http://www.w3.org/2003/05/soap-rpc" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"><soap12:Body soap12:encodingStyle="http://www.w3.org/2003/05/soap-encoding"><q4:procesaNotificacionSISResponse xmlns:q4="InotificacionSIS"><rpc:result xmlns="">return</rpc:result><return xsi:type="xsd:string">${escapedAnswer}</return></q4:procesaNotificacionSISResponse></soap12:Body></soap12:Envelope>`;
|
|
655
|
-
};
|
|
656
|
-
// Range: 0 to (top -1)
|
|
657
|
-
const randomInt = (top) => {
|
|
658
|
-
return Math.floor(Math.random() * Math.floor(top));
|
|
659
|
-
};
|
|
660
|
-
const alphanumeric = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
661
|
-
const randomTransactionId = () => {
|
|
662
|
-
// Random 4 digit number between 0 and 9999
|
|
663
|
-
const num = randomInt(10000);
|
|
664
|
-
const numStr = num.toString().padStart(4, '0');
|
|
665
|
-
// Random 8 letter alphanumeric sequence
|
|
666
|
-
const alphanum = new Array(8);
|
|
667
|
-
for (let idx = 0; idx < 8; idx++) {
|
|
668
|
-
alphanum[idx] = alphanumeric[randomInt(alphanumeric.length)];
|
|
669
|
-
}
|
|
670
|
-
const alphanumStr = alphanum.join('');
|
|
671
|
-
return `${numStr}${alphanumStr}`;
|
|
672
1099
|
};
|
|
673
1100
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
de: '5',
|
|
681
|
-
nl: '6',
|
|
682
|
-
it: '7',
|
|
683
|
-
sv: '8',
|
|
684
|
-
pt: '9',
|
|
685
|
-
pl: '11',
|
|
686
|
-
gl: '12',
|
|
687
|
-
eu: '13',
|
|
688
|
-
// Redsys ha utilizado ISO 3166-1(pais) como codigo de idioma
|
|
689
|
-
bg: '100',
|
|
690
|
-
zh: '156',
|
|
691
|
-
hr: '191',
|
|
692
|
-
cs: '203',
|
|
693
|
-
da: '208',
|
|
694
|
-
et: '233',
|
|
695
|
-
fi: '246',
|
|
696
|
-
el: '300',
|
|
697
|
-
hu: '348',
|
|
698
|
-
ja: '392',
|
|
699
|
-
lv: '428',
|
|
700
|
-
lt: '440',
|
|
701
|
-
mt: '470',
|
|
702
|
-
ro: '642',
|
|
703
|
-
ru: '643',
|
|
704
|
-
sk: '703',
|
|
705
|
-
sl: '705',
|
|
706
|
-
tr: '792'
|
|
707
|
-
};
|
|
708
|
-
const REV_LANGUAGES = {};
|
|
709
|
-
for (const [key, value] of Object.entries(LANGUAGES)) {
|
|
710
|
-
REV_LANGUAGES[value] = key;
|
|
711
|
-
}
|
|
712
|
-
REV_LANGUAGES['10'] = 'ca';
|
|
713
|
-
|
|
714
|
-
// ISO 4217
|
|
1101
|
+
/**
|
|
1102
|
+
* Maps a ISO 4217 code to a ISO 4217 currency number and decimal positions
|
|
1103
|
+
*
|
|
1104
|
+
* @public
|
|
1105
|
+
*/
|
|
1106
|
+
// The Record key makes sure that all members of the key union type are defined.
|
|
715
1107
|
const CURRENCIES = {
|
|
716
1108
|
ALL: { num: '8', decimals: 2 },
|
|
717
1109
|
DZD: { num: '12', decimals: 2 },
|
|
@@ -925,6 +1317,15 @@ const CURRENCIES = {
|
|
|
925
1317
|
BRL: { num: '986', decimals: 2 }
|
|
926
1318
|
// ZAL: { num: '991', decimals: }
|
|
927
1319
|
};
|
|
1320
|
+
/**
|
|
1321
|
+
* Maps a ISO 4217 number to a ISO 4217 currency code and decimal positions
|
|
1322
|
+
*
|
|
1323
|
+
* @remarks
|
|
1324
|
+
* @see {@link CURRENCIES}
|
|
1325
|
+
*
|
|
1326
|
+
* @public
|
|
1327
|
+
*/
|
|
1328
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
928
1329
|
const REV_CURRENCIES = {};
|
|
929
1330
|
for (const [key, value] of Object.entries(CURRENCIES)) {
|
|
930
1331
|
REV_CURRENCIES[value.num] = {
|
|
@@ -933,7 +1334,72 @@ for (const [key, value] of Object.entries(CURRENCIES)) {
|
|
|
933
1334
|
};
|
|
934
1335
|
}
|
|
935
1336
|
|
|
936
|
-
|
|
1337
|
+
/**
|
|
1338
|
+
* Maps an ISO 639-1 language code to redsys language number
|
|
1339
|
+
*
|
|
1340
|
+
* @remarks
|
|
1341
|
+
* Some ISO 639-1 language codes may include more more than one redsys language number. In such cases the main redsys language number is used.
|
|
1342
|
+
*
|
|
1343
|
+
* @public
|
|
1344
|
+
*/
|
|
1345
|
+
// The Record key makes sure that all members of the key union type are defined.
|
|
1346
|
+
const LANGUAGES = {
|
|
1347
|
+
es: '1',
|
|
1348
|
+
en: '2',
|
|
1349
|
+
ca: '3',
|
|
1350
|
+
fr: '4',
|
|
1351
|
+
de: '5',
|
|
1352
|
+
nl: '6',
|
|
1353
|
+
it: '7',
|
|
1354
|
+
sv: '8',
|
|
1355
|
+
pt: '9',
|
|
1356
|
+
pl: '11',
|
|
1357
|
+
gl: '12',
|
|
1358
|
+
eu: '13',
|
|
1359
|
+
// Redsys ha utilizado ISO 3166-1(pais) como codigo de idioma para el resto
|
|
1360
|
+
bg: '100',
|
|
1361
|
+
zh: '156',
|
|
1362
|
+
hr: '191',
|
|
1363
|
+
cs: '203',
|
|
1364
|
+
da: '208',
|
|
1365
|
+
et: '233',
|
|
1366
|
+
fi: '246',
|
|
1367
|
+
el: '300',
|
|
1368
|
+
hu: '348',
|
|
1369
|
+
ja: '392',
|
|
1370
|
+
lv: '428',
|
|
1371
|
+
lt: '440',
|
|
1372
|
+
mt: '470',
|
|
1373
|
+
ro: '642',
|
|
1374
|
+
ru: '643',
|
|
1375
|
+
sk: '703',
|
|
1376
|
+
sl: '705',
|
|
1377
|
+
tr: '792'
|
|
1378
|
+
};
|
|
1379
|
+
/**
|
|
1380
|
+
* Maps a redsys language code to an ISO 639-1 language code
|
|
1381
|
+
*
|
|
1382
|
+
* @remarks
|
|
1383
|
+
* @see {@link LANGUAGES}
|
|
1384
|
+
*
|
|
1385
|
+
* @public
|
|
1386
|
+
*/
|
|
1387
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
1388
|
+
const REV_LANGUAGES = {};
|
|
1389
|
+
for (const [key, value] of Object.entries(LANGUAGES)) {
|
|
1390
|
+
REV_LANGUAGES[value] = key;
|
|
1391
|
+
}
|
|
1392
|
+
REV_LANGUAGES['10'] = 'ca';
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Maps ISO 3166 Alpha 2 codes to UN M49 numbers
|
|
1396
|
+
*
|
|
1397
|
+
* @remarks
|
|
1398
|
+
* [UN M49 to ISO 3166 information](https://unstats.un.org/unsd/methodology/m49/)
|
|
1399
|
+
*
|
|
1400
|
+
* @public
|
|
1401
|
+
*/
|
|
1402
|
+
// The Record key makes sure that all members of the key union type are defined.
|
|
937
1403
|
const COUNTRIES = {
|
|
938
1404
|
af: '4',
|
|
939
1405
|
al: '8',
|
|
@@ -1185,344 +1651,26 @@ const COUNTRIES = {
|
|
|
1185
1651
|
ye: '887',
|
|
1186
1652
|
zm: '894'
|
|
1187
1653
|
};
|
|
1654
|
+
/**
|
|
1655
|
+
* Maps UN M49 numbers to ISO 3166 Alpha 2 codes
|
|
1656
|
+
*
|
|
1657
|
+
* @remarks
|
|
1658
|
+
* @see {@link COUNTRIES}
|
|
1659
|
+
*
|
|
1660
|
+
* @public
|
|
1661
|
+
*/
|
|
1662
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
1188
1663
|
const REV_COUNTRIES = {};
|
|
1189
1664
|
for (const [key, value] of Object.entries(COUNTRIES)) {
|
|
1190
1665
|
REV_COUNTRIES[value] = key;
|
|
1191
1666
|
}
|
|
1192
1667
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
case 'expiryYear':
|
|
1200
|
-
case 'raw':
|
|
1201
|
-
continue;
|
|
1202
|
-
case 'order':
|
|
1203
|
-
obj.DS_MERCHANT_ORDER = input[key];
|
|
1204
|
-
break;
|
|
1205
|
-
case 'merchantCode':
|
|
1206
|
-
obj.DS_MERCHANT_MERCHANTCODE = input[key];
|
|
1207
|
-
break;
|
|
1208
|
-
case 'transactionType':
|
|
1209
|
-
obj.DS_MERCHANT_TRANSACTIONTYPE = input.transactionType;
|
|
1210
|
-
break;
|
|
1211
|
-
case 'terminal': {
|
|
1212
|
-
const value = input[key];
|
|
1213
|
-
if (!isStringNotEmpty(value))
|
|
1214
|
-
continue;
|
|
1215
|
-
obj.DS_MERCHANT_TERMINAL = value;
|
|
1216
|
-
break;
|
|
1217
|
-
}
|
|
1218
|
-
case 'currency': {
|
|
1219
|
-
const value = input.currency;
|
|
1220
|
-
if (!isStringNotEmpty(value))
|
|
1221
|
-
continue;
|
|
1222
|
-
const currencyData = CURRENCIES[value];
|
|
1223
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
1224
|
-
if (!currencyData || !currencyData.num) {
|
|
1225
|
-
throw new ValidationError('Unsupported currency', value, 'currency');
|
|
1226
|
-
}
|
|
1227
|
-
obj.DS_MERCHANT_CURRENCY = currencyData.num.padStart(3, '0');
|
|
1228
|
-
break;
|
|
1229
|
-
}
|
|
1230
|
-
case 'amount': {
|
|
1231
|
-
/*
|
|
1232
|
-
* Experimental feature!!!
|
|
1233
|
-
*
|
|
1234
|
-
* The feature amountType='float' may be removed any time.
|
|
1235
|
-
* If you like this feature or have any comment please open an issue
|
|
1236
|
-
*
|
|
1237
|
-
* amountType can be atomic or float.
|
|
1238
|
-
* - atomic: amount represents the smallest currency unit. 1130 (EUR) -> 11.30 EUR
|
|
1239
|
-
* - float: amount is rounded to last decimal precision. 11.299999999 (EUR) -> 11.30 EUR
|
|
1240
|
-
*
|
|
1241
|
-
* Float amount can lead to losing precision, specially after doing
|
|
1242
|
-
* operations. Atomic is more precise for monetary amount, no lost cents,
|
|
1243
|
-
* better for accounting.
|
|
1244
|
-
*
|
|
1245
|
-
*/
|
|
1246
|
-
const value = input[key];
|
|
1247
|
-
// atomic by default
|
|
1248
|
-
let rawValue = value.toString();
|
|
1249
|
-
if ((options === null || options === void 0 ? void 0 : options.amountType) != null) {
|
|
1250
|
-
if (options.amountType === 'float') {
|
|
1251
|
-
const currencyData = CURRENCIES[input.currency];
|
|
1252
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
1253
|
-
if (!currencyData || !currencyData.decimals) {
|
|
1254
|
-
throw new ValidationError('Unsupported currency', input.currency, 'currency');
|
|
1255
|
-
}
|
|
1256
|
-
const numericValue = typeof value === 'number' ? value : Number.parseFloat(value);
|
|
1257
|
-
if (!Number.isFinite(numericValue)) {
|
|
1258
|
-
throw new ValidationError('Invalid amount', value, 'amount');
|
|
1259
|
-
}
|
|
1260
|
-
rawValue = Math.round(numericValue * Math.pow(10, currencyData.decimals)).toString();
|
|
1261
|
-
}
|
|
1262
|
-
else if (options.amountType !== 'atomic') {
|
|
1263
|
-
throw new ValidationError('Unknown format', options.amountType, 'amountType');
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
if (rawValue.length > 12) {
|
|
1267
|
-
throw new ValidationError('Amount to charge is too large', value, 'amount');
|
|
1268
|
-
}
|
|
1269
|
-
obj.DS_MERCHANT_AMOUNT = rawValue;
|
|
1270
|
-
break;
|
|
1271
|
-
}
|
|
1272
|
-
case 'merchantName': {
|
|
1273
|
-
const value = input[key];
|
|
1274
|
-
if (!isStringNotEmpty(value))
|
|
1275
|
-
continue;
|
|
1276
|
-
obj.DS_MERCHANT_MERCHANTNAME = value;
|
|
1277
|
-
break;
|
|
1278
|
-
}
|
|
1279
|
-
case 'merchantURL': {
|
|
1280
|
-
const value = input[key];
|
|
1281
|
-
if (!isStringNotEmpty(value))
|
|
1282
|
-
continue;
|
|
1283
|
-
obj.DS_MERCHANT_MERCHANTURL = value;
|
|
1284
|
-
break;
|
|
1285
|
-
}
|
|
1286
|
-
case 'merchantSignature': {
|
|
1287
|
-
const value = input[key];
|
|
1288
|
-
if (!isStringNotEmpty(value))
|
|
1289
|
-
continue;
|
|
1290
|
-
obj.DS_MERCHANT_MERCHANTSIGNATURE = value;
|
|
1291
|
-
break;
|
|
1292
|
-
}
|
|
1293
|
-
case 'successURL': {
|
|
1294
|
-
const value = input[key];
|
|
1295
|
-
if (!isStringNotEmpty(value))
|
|
1296
|
-
continue;
|
|
1297
|
-
obj.DS_MERCHANT_URLOK = value;
|
|
1298
|
-
break;
|
|
1299
|
-
}
|
|
1300
|
-
case 'errorURL': {
|
|
1301
|
-
const value = input[key];
|
|
1302
|
-
if (!isStringNotEmpty(value))
|
|
1303
|
-
continue;
|
|
1304
|
-
obj.DS_MERCHANT_URLKO = value;
|
|
1305
|
-
break;
|
|
1306
|
-
}
|
|
1307
|
-
case 'dateFrequency': {
|
|
1308
|
-
const value = input[key];
|
|
1309
|
-
if (!isStringNotEmpty(value))
|
|
1310
|
-
continue;
|
|
1311
|
-
obj.DS_MERCHANT_DATEFRECUENCY = value;
|
|
1312
|
-
break;
|
|
1313
|
-
}
|
|
1314
|
-
case 'chargeExpiryDate': {
|
|
1315
|
-
const value = input[key];
|
|
1316
|
-
if (!isStringNotEmpty(value))
|
|
1317
|
-
continue;
|
|
1318
|
-
obj.DS_MERCHANT_CHARGEEXPIRYDATE = value;
|
|
1319
|
-
break;
|
|
1320
|
-
}
|
|
1321
|
-
case 'sumTotal': {
|
|
1322
|
-
const value = input[key];
|
|
1323
|
-
if (!isStringNotEmpty(value))
|
|
1324
|
-
continue;
|
|
1325
|
-
obj.DS_MERCHANT_SUMTOTAL = value;
|
|
1326
|
-
break;
|
|
1327
|
-
}
|
|
1328
|
-
case 'directPayment': {
|
|
1329
|
-
const value = input[key];
|
|
1330
|
-
if (!isStringNotEmpty(value))
|
|
1331
|
-
continue;
|
|
1332
|
-
obj.DS_MERCHANT_DIRECTPAYMENT = value;
|
|
1333
|
-
break;
|
|
1334
|
-
}
|
|
1335
|
-
case 'identifier': {
|
|
1336
|
-
const value = input[key];
|
|
1337
|
-
if (!isStringNotEmpty(value))
|
|
1338
|
-
continue;
|
|
1339
|
-
obj.DS_MERCHANT_IDENTIFIER = value;
|
|
1340
|
-
break;
|
|
1341
|
-
}
|
|
1342
|
-
case 'group': {
|
|
1343
|
-
const value = input[key];
|
|
1344
|
-
if (!isStringNotEmpty(value))
|
|
1345
|
-
continue;
|
|
1346
|
-
obj.DS_MERCHANT_GROUP = value;
|
|
1347
|
-
break;
|
|
1348
|
-
}
|
|
1349
|
-
case 'pan': {
|
|
1350
|
-
const value = input[key];
|
|
1351
|
-
if (!isStringNotEmpty(value))
|
|
1352
|
-
continue;
|
|
1353
|
-
obj.DS_MERCHANT_PAN = value;
|
|
1354
|
-
break;
|
|
1355
|
-
}
|
|
1356
|
-
case 'expiryDate': {
|
|
1357
|
-
const stdFmt = input[key];
|
|
1358
|
-
if (!isStringNotEmpty(stdFmt))
|
|
1359
|
-
continue;
|
|
1360
|
-
if (stdFmt.length !== 4 || !/^\d+$/.test(stdFmt)) {
|
|
1361
|
-
throw new ValidationError('Invalid expiryDate', stdFmt, 'expiryDate');
|
|
1362
|
-
}
|
|
1363
|
-
const altFmt = `${stdFmt.slice(2, 4)}${stdFmt.slice(0, 2)}`;
|
|
1364
|
-
obj.DS_MERCHANT_EXPIRYDATE = altFmt;
|
|
1365
|
-
break;
|
|
1366
|
-
}
|
|
1367
|
-
case 'cvv': {
|
|
1368
|
-
const value = input[key];
|
|
1369
|
-
if (!isStringNotEmpty(value))
|
|
1370
|
-
continue;
|
|
1371
|
-
obj.DS_MERCHANT_CVV2 = value;
|
|
1372
|
-
break;
|
|
1373
|
-
}
|
|
1374
|
-
case 'cardCountry': {
|
|
1375
|
-
const value = input[key];
|
|
1376
|
-
if (!isStringNotEmpty(value))
|
|
1377
|
-
continue;
|
|
1378
|
-
const countryInt = COUNTRIES[value];
|
|
1379
|
-
if (countryInt != null && countryInt) {
|
|
1380
|
-
obj.DS_CARD_COUNTRY = countryInt;
|
|
1381
|
-
}
|
|
1382
|
-
break;
|
|
1383
|
-
}
|
|
1384
|
-
case 'lang': {
|
|
1385
|
-
const value = input[key];
|
|
1386
|
-
if (!isStringNotEmpty(value))
|
|
1387
|
-
continue;
|
|
1388
|
-
const langInt = LANGUAGES[value];
|
|
1389
|
-
if (langInt != null && langInt) {
|
|
1390
|
-
obj.DS_MERCHANT_CONSUMERLANGUAGE = langInt;
|
|
1391
|
-
}
|
|
1392
|
-
break;
|
|
1393
|
-
}
|
|
1394
|
-
case 'merchantData': {
|
|
1395
|
-
const value = input[key];
|
|
1396
|
-
if (!isStringNotEmpty(value))
|
|
1397
|
-
continue;
|
|
1398
|
-
obj.DS_MERCHANT_MERCHANTDATA = value;
|
|
1399
|
-
break;
|
|
1400
|
-
}
|
|
1401
|
-
case 'clientIp': {
|
|
1402
|
-
const value = input[key];
|
|
1403
|
-
if (!isStringNotEmpty(value))
|
|
1404
|
-
continue;
|
|
1405
|
-
obj.DS_MERCHANT_CLIENTIP = value;
|
|
1406
|
-
break;
|
|
1407
|
-
}
|
|
1408
|
-
case 'operationId': {
|
|
1409
|
-
const value = input[key];
|
|
1410
|
-
if (!isStringNotEmpty(value))
|
|
1411
|
-
continue;
|
|
1412
|
-
obj.DS_MERCHANT_IDOPER = value;
|
|
1413
|
-
break;
|
|
1414
|
-
}
|
|
1415
|
-
case 'payMethods': {
|
|
1416
|
-
const value = input[key];
|
|
1417
|
-
if (!isStringNotEmpty(value))
|
|
1418
|
-
continue;
|
|
1419
|
-
obj.DS_MERCHANT_PAYMETHODS = value;
|
|
1420
|
-
break;
|
|
1421
|
-
}
|
|
1422
|
-
case 'productDescription': {
|
|
1423
|
-
const value = input[key];
|
|
1424
|
-
if (!isStringNotEmpty(value))
|
|
1425
|
-
continue;
|
|
1426
|
-
obj.DS_MERCHANT_PRODUCTDESCRIPTION = value;
|
|
1427
|
-
break;
|
|
1428
|
-
}
|
|
1429
|
-
case 'taxReference': {
|
|
1430
|
-
const value = input[key];
|
|
1431
|
-
if (!isStringNotEmpty(value))
|
|
1432
|
-
continue;
|
|
1433
|
-
obj.DS_MERCHANT_TAX_REFERENCE = value;
|
|
1434
|
-
break;
|
|
1435
|
-
}
|
|
1436
|
-
case 'transactionDate': {
|
|
1437
|
-
const value = input[key];
|
|
1438
|
-
if (!isStringNotEmpty(value))
|
|
1439
|
-
continue;
|
|
1440
|
-
obj.DS_MERCHANT_TRANSACTIONDATE = value;
|
|
1441
|
-
break;
|
|
1442
|
-
}
|
|
1443
|
-
case 'merchantDescriptor': {
|
|
1444
|
-
const value = input[key];
|
|
1445
|
-
if (!isStringNotEmpty(value))
|
|
1446
|
-
continue;
|
|
1447
|
-
obj.DS_MERCHANT_MERCHANTDESCRIPTOR = value;
|
|
1448
|
-
break;
|
|
1449
|
-
}
|
|
1450
|
-
case 'customerMobile': {
|
|
1451
|
-
const value = input[key];
|
|
1452
|
-
if (!isStringNotEmpty(value))
|
|
1453
|
-
continue;
|
|
1454
|
-
obj.DS_MERCHANT_CUSTOMER_MOBILE = value;
|
|
1455
|
-
break;
|
|
1456
|
-
}
|
|
1457
|
-
case 'customerMail': {
|
|
1458
|
-
const value = input[key];
|
|
1459
|
-
if (!isStringNotEmpty(value))
|
|
1460
|
-
continue;
|
|
1461
|
-
obj.DS_MERCHANT_CUSTOMER_MAIL = value;
|
|
1462
|
-
break;
|
|
1463
|
-
}
|
|
1464
|
-
case 'cardHolder': {
|
|
1465
|
-
const value = input[key];
|
|
1466
|
-
if (!isStringNotEmpty(value))
|
|
1467
|
-
continue;
|
|
1468
|
-
obj.DS_MERCHANT_TITULAR = value;
|
|
1469
|
-
break;
|
|
1470
|
-
}
|
|
1471
|
-
case 'smsTemplate': {
|
|
1472
|
-
const value = input[key];
|
|
1473
|
-
if (!isStringNotEmpty(value))
|
|
1474
|
-
continue;
|
|
1475
|
-
obj.DS_MERCHANT_CUSTOMER_SMS_TEXT = value;
|
|
1476
|
-
break;
|
|
1477
|
-
}
|
|
1478
|
-
default:
|
|
1479
|
-
throw new ValidationError('Unknown parameter', undefined, key);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
return obj;
|
|
1483
|
-
};
|
|
1484
|
-
const formatParams = (paramsInput, options) => {
|
|
1485
|
-
// Pre processing
|
|
1486
|
-
const { amount, currency, merchantCode, transactionType, order, expiryYear, expiryMonth, expiryDate } = paramsInput;
|
|
1487
|
-
if ((typeof amount !== 'string' || !amount) &&
|
|
1488
|
-
(typeof amount !== 'number' || amount < 0)) {
|
|
1489
|
-
// An amount of 0 is valid, it may be used for obtaining a credit card reference(tokenization)
|
|
1490
|
-
throw new ValidationError('Invalid amount', amount, 'amount');
|
|
1491
|
-
}
|
|
1492
|
-
if (!currency) {
|
|
1493
|
-
throw new ValidationError('No currency provided', currency, 'currency');
|
|
1494
|
-
}
|
|
1495
|
-
if (!merchantCode) {
|
|
1496
|
-
throw new ValidationError('The merchant code is mandatory', merchantCode, 'merchantCode');
|
|
1497
|
-
}
|
|
1498
|
-
if (!transactionType) {
|
|
1499
|
-
throw new ValidationError('The transaction type is mandatory', transactionType, 'transactionType');
|
|
1500
|
-
}
|
|
1501
|
-
if (!order) {
|
|
1502
|
-
throw new ValidationError('No order reference provided', order, 'order');
|
|
1503
|
-
}
|
|
1504
|
-
if ((expiryDate == null || !expiryDate) &&
|
|
1505
|
-
(expiryMonth != null && expiryMonth) &&
|
|
1506
|
-
(expiryYear != null && expiryYear)) {
|
|
1507
|
-
if (expiryMonth.length !== 2) {
|
|
1508
|
-
throw new ValidationError('Invalid expiryMonth', expiryMonth, 'expiryMonth');
|
|
1509
|
-
}
|
|
1510
|
-
if (expiryYear.length !== 2) {
|
|
1511
|
-
throw new ValidationError('Invalid expiryYear', expiryYear, 'expiryYear');
|
|
1512
|
-
}
|
|
1513
|
-
paramsInput.expiryDate = `${expiryMonth}${expiryYear}`;
|
|
1514
|
-
}
|
|
1515
|
-
// Defaults
|
|
1516
|
-
if (paramsInput.terminal == null || !paramsInput.terminal)
|
|
1517
|
-
paramsInput.terminal = '1';
|
|
1518
|
-
const paramsObj = inputParamsFormatter(paramsInput, options);
|
|
1519
|
-
return {
|
|
1520
|
-
...paramsObj,
|
|
1521
|
-
// Last, so raw doesn't get overwritten
|
|
1522
|
-
...paramsInput.raw
|
|
1523
|
-
};
|
|
1524
|
-
};
|
|
1525
|
-
|
|
1668
|
+
/**
|
|
1669
|
+
* Card brand name to number
|
|
1670
|
+
*
|
|
1671
|
+
* @public
|
|
1672
|
+
*/
|
|
1673
|
+
// The Record key makes sure that all members of the key union type are defined.
|
|
1526
1674
|
const CARDBRANDS = {
|
|
1527
1675
|
VISA: '1',
|
|
1528
1676
|
MASTERCARD: '2',
|
|
@@ -1532,300 +1680,363 @@ const CARDBRANDS = {
|
|
|
1532
1680
|
JCB: '9',
|
|
1533
1681
|
UPI: '22'
|
|
1534
1682
|
};
|
|
1683
|
+
/**
|
|
1684
|
+
* Card brand number to name
|
|
1685
|
+
*
|
|
1686
|
+
* @remarks
|
|
1687
|
+
* @see {@link CARDBRANDS}
|
|
1688
|
+
*
|
|
1689
|
+
* @public
|
|
1690
|
+
*/
|
|
1691
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
1535
1692
|
const REV_CARDBRANDS = {};
|
|
1536
1693
|
for (const [key, value] of Object.entries(CARDBRANDS)) {
|
|
1537
1694
|
REV_CARDBRANDS[value] = key;
|
|
1538
1695
|
}
|
|
1539
1696
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
obj.merchantData = value;
|
|
1589
|
-
},
|
|
1590
|
-
DS_SECUREPAYMENT: (obj, value) => {
|
|
1591
|
-
obj.securePayment = value === '1';
|
|
1592
|
-
},
|
|
1593
|
-
DS_TRANSACTIONTYPE: (obj, value) => {
|
|
1594
|
-
obj.transactionType = value;
|
|
1595
|
-
},
|
|
1596
|
-
// Pago por referencia
|
|
1597
|
-
DS_MERCHANT_IDENTIFIER: (obj, value) => {
|
|
1598
|
-
obj.identifier = value;
|
|
1599
|
-
},
|
|
1600
|
-
DS_MERCHANT_GROUP: (obj, value) => {
|
|
1601
|
-
obj.merchantGroup = value;
|
|
1602
|
-
},
|
|
1603
|
-
DS_EXPIRYDATE: (obj, value) => {
|
|
1604
|
-
obj.expiryMonth = value.slice(2, 4);
|
|
1605
|
-
obj.expiryYear = value.slice(0, 2);
|
|
1606
|
-
obj.expiryDate = `${obj.expiryMonth}${obj.expiryYear}`;
|
|
1607
|
-
},
|
|
1608
|
-
// Optional
|
|
1609
|
-
DS_CARDNUMBER: (obj, value) => {
|
|
1610
|
-
obj.cardNumber = value;
|
|
1611
|
-
},
|
|
1612
|
-
DS_CARD_TYPE: (obj, value) => {
|
|
1613
|
-
obj.cardType = value;
|
|
1614
|
-
},
|
|
1615
|
-
DS_AUTHORISATIONCODE: (obj, value) => {
|
|
1616
|
-
obj.authorisationCode = value;
|
|
1617
|
-
},
|
|
1618
|
-
DS_CONSUMERLANGUAGE: langFormatter,
|
|
1619
|
-
DS_LANGUAGE: langFormatter,
|
|
1620
|
-
DS_CARD_COUNTRY: (obj, value) => {
|
|
1621
|
-
const cardCountryInt = Number.parseInt(value);
|
|
1622
|
-
const cardCountry = REV_COUNTRIES[cardCountryInt];
|
|
1623
|
-
if (cardCountry != null && cardCountry) {
|
|
1624
|
-
obj.cardCountry = cardCountry;
|
|
1625
|
-
}
|
|
1626
|
-
},
|
|
1627
|
-
DS_CARD_BRAND: (obj, value) => {
|
|
1628
|
-
const cardBrandInt = Number.parseInt(value);
|
|
1629
|
-
const cardBrand = REV_CARDBRANDS[cardBrandInt];
|
|
1630
|
-
if (cardBrand != null && cardBrand) {
|
|
1631
|
-
obj.cardBrand = cardBrand;
|
|
1632
|
-
}
|
|
1633
|
-
},
|
|
1634
|
-
DS_URLPAGO2FASES: (obj, value) => {
|
|
1635
|
-
obj.payURL = value;
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
const formatResponse = (rawParams) => {
|
|
1639
|
-
const obj = { raw: rawParams };
|
|
1640
|
-
for (const [key, value] of Object.entries(rawParams)) {
|
|
1641
|
-
const formatter = responseFormatters[key.toUpperCase()];
|
|
1642
|
-
if (!formatter)
|
|
1643
|
-
continue;
|
|
1644
|
-
formatter(obj, value);
|
|
1645
|
-
}
|
|
1646
|
-
// Post formatting
|
|
1647
|
-
if (obj.date != null && obj.date && obj.hour != null && obj.hour) {
|
|
1648
|
-
obj.timestamp = dayjs.tz(`${obj.date} ${obj.hour}`, 'YYYY-MM-DD HH:mm', 'Europe/Madrid').toDate();
|
|
1649
|
-
}
|
|
1650
|
-
return obj;
|
|
1697
|
+
/**
|
|
1698
|
+
* Transaction type enum
|
|
1699
|
+
*
|
|
1700
|
+
* @public
|
|
1701
|
+
*/
|
|
1702
|
+
const TRANSACTION_TYPES = {
|
|
1703
|
+
/** Autorización */
|
|
1704
|
+
AUTHORIZATION: '0',
|
|
1705
|
+
/** Preautorización */
|
|
1706
|
+
PRE_AUTHORIZATION: '1',
|
|
1707
|
+
/** Confirmación de preautorización */
|
|
1708
|
+
CONFIRMATION: '2',
|
|
1709
|
+
/** Devolución Automática */
|
|
1710
|
+
AUTO_REFUND: '3',
|
|
1711
|
+
/** Transacción Recurrente */
|
|
1712
|
+
RECURRING_TRANSACTION: '5',
|
|
1713
|
+
/** Transacción Sucesiva */
|
|
1714
|
+
SUCCESSIVE_TRANSACTION: '6',
|
|
1715
|
+
/** Pre-autenticación */
|
|
1716
|
+
PRE_AUTHENTICATION: '7',
|
|
1717
|
+
/** Confirmación de pre-autenticación */
|
|
1718
|
+
PRE_AUTHENTICATION_CONFIRMATION: '8',
|
|
1719
|
+
/** Anulación de Preautorización */
|
|
1720
|
+
PRE_AUTHORIZATION_CANCEL: '9',
|
|
1721
|
+
/** Autorización en diferido */
|
|
1722
|
+
DEFERRED_AUTHORIZATION: 'O',
|
|
1723
|
+
/** Paygold */
|
|
1724
|
+
PAYGOLD: '15',
|
|
1725
|
+
/** Autenticación Puce */
|
|
1726
|
+
PUCE_AUTH: '17',
|
|
1727
|
+
/** Devolución sin original */
|
|
1728
|
+
RETURN_WITHOUT_ORIGINAL: '34',
|
|
1729
|
+
/** Premio de apuestas */
|
|
1730
|
+
BETTING_PAYOUT: '37',
|
|
1731
|
+
/** Eliminar referencia */
|
|
1732
|
+
DELETE_REFERENCE: '44',
|
|
1733
|
+
/** Operaciones sin autenticacíon */
|
|
1734
|
+
NO_AUTHENTICATION: 'A',
|
|
1735
|
+
/** Paygold */
|
|
1736
|
+
PAYLINK: 'F',
|
|
1737
|
+
/** Confirmación de autorización en diferido */
|
|
1738
|
+
DEFERRED_AUTHORIZATION_CONFIRMATION: 'P',
|
|
1739
|
+
/** Anulación de autorización en diferido */
|
|
1740
|
+
DEFERRED_AUTHORIZATION_CANCEL: 'Q',
|
|
1741
|
+
/** Cuota inicial diferido */
|
|
1742
|
+
DEFERRED_INITIAL_FEE: 'R',
|
|
1743
|
+
/** Cuota sucesiva diferido */
|
|
1744
|
+
DEFERRED_SUCCESSIVE_FEE: 'S'
|
|
1651
1745
|
};
|
|
1652
1746
|
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1747
|
+
/**
|
|
1748
|
+
* Applies an input formatter to the first argument of a function
|
|
1749
|
+
*
|
|
1750
|
+
* @remarks
|
|
1751
|
+
* The signature of this function is so complex because tries to suport
|
|
1752
|
+
* formatters with generics that provide a default type. This should remain
|
|
1753
|
+
* until [variadic generics](https://github.com/microsoft/TypeScript/issues/5453)
|
|
1754
|
+
* land in typescript
|
|
1755
|
+
*
|
|
1756
|
+
* @public
|
|
1757
|
+
*/
|
|
1758
|
+
const useSingleInputFormatter = (fn, inputFormatter) => {
|
|
1759
|
+
return (input, ...otherArgs) => fn(inputFormatter(input), ...otherArgs);
|
|
1657
1760
|
};
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1761
|
+
/**
|
|
1762
|
+
* Applies an output formatter to a function
|
|
1763
|
+
*
|
|
1764
|
+
* @public
|
|
1765
|
+
*/
|
|
1766
|
+
const useOutputFormatter = (fn, outputFormatter) => {
|
|
1767
|
+
return (a) => outputFormatter(fn(a));
|
|
1661
1768
|
};
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
}
|
|
1677
|
-
redirectPetitionParameters(paramsInput, options) {
|
|
1678
|
-
const paramsObj = formatParams(paramsInput, options);
|
|
1679
|
-
// Docs escape "/" but we don't, this JSON won't get placed in a script tag
|
|
1680
|
-
return Buffer.from(JSON.stringify(paramsObj), 'utf8').toString('base64');
|
|
1681
|
-
}
|
|
1682
|
-
redirectPetition(paramsInput, options) {
|
|
1683
|
-
const paramsString = this.redirectPetitionParameters(paramsInput, options);
|
|
1684
|
-
const signature = sha256Sign(this.secretKey, paramsInput.order, paramsString);
|
|
1685
|
-
return {
|
|
1686
|
-
url: this.urls.redirect,
|
|
1687
|
-
body: {
|
|
1688
|
-
Ds_SignatureVersion: 'HMAC_SHA256_V1',
|
|
1689
|
-
Ds_MerchantParameters: paramsString,
|
|
1690
|
-
Ds_Signature: signature
|
|
1691
|
-
}
|
|
1692
|
-
};
|
|
1693
|
-
}
|
|
1694
|
-
processNotificationParameters(strPayload) {
|
|
1695
|
-
if (!strPayload) {
|
|
1696
|
-
throw new ParseError('Payload is required');
|
|
1697
|
-
}
|
|
1698
|
-
if (typeof strPayload !== 'string') {
|
|
1699
|
-
throw new ParseError('Payload must be a base-64 encoded string');
|
|
1700
|
-
}
|
|
1701
|
-
const payload = JSON.parse(decodeURIComponent(Buffer.from(strPayload, 'base64').toString('utf8')));
|
|
1702
|
-
if (typeof payload !== 'object' || payload == null) {
|
|
1703
|
-
throw new ParseError('Cannot parse notification payload');
|
|
1704
|
-
}
|
|
1705
|
-
return payload;
|
|
1706
|
-
}
|
|
1707
|
-
processNotification(body) {
|
|
1708
|
-
const params = this.processNotificationParameters(body.Ds_MerchantParameters);
|
|
1709
|
-
const order = params.Ds_Order;
|
|
1710
|
-
if (order == null || !order) {
|
|
1711
|
-
throw new ParseError('Invalid notification parameters');
|
|
1712
|
-
}
|
|
1713
|
-
const signature = body.Ds_Signature;
|
|
1714
|
-
// Base64url with padding, only substitutes + and /
|
|
1715
|
-
const expSignature = sha256Sign(this.secretKey, order, body.Ds_MerchantParameters);
|
|
1716
|
-
// Comparing different base64 encodings is messy. Foolproof, compare buffers.
|
|
1717
|
-
if (!signature || !Buffer.from(expSignature, 'base64').equals(base64url.toBuffer(signature))) {
|
|
1718
|
-
throw new ParseError('Invalid signature');
|
|
1719
|
-
}
|
|
1720
|
-
return formatResponse(params);
|
|
1721
|
-
}
|
|
1722
|
-
processXMLResponseData(xmlData) {
|
|
1723
|
-
const parsedResponse = xmlParser.parse(xmlData, { parseNodeValue: false });
|
|
1724
|
-
if (typeof parsedResponse !== 'object' || parsedResponse == null) {
|
|
1725
|
-
throw new ParseError('Cannot parse notification payload');
|
|
1726
|
-
}
|
|
1727
|
-
return parsedResponse.RETORNOXML;
|
|
1728
|
-
}
|
|
1729
|
-
// Generic Processor for Web Service Responses
|
|
1730
|
-
processXMLResponse(xmlData) {
|
|
1731
|
-
const data = this.processXMLResponseData(xmlData);
|
|
1732
|
-
if (data.CODIGO !== '0') {
|
|
1733
|
-
const err = new ParseError(`Redsys error ${data.CODIGO}`);
|
|
1734
|
-
err.code = data.CODIGO;
|
|
1735
|
-
err.description = getSISErrorCodeMessage(data.CODIGO);
|
|
1736
|
-
// Can't access data.OPERACION, only data.RECIBIDO
|
|
1737
|
-
// However data.RECIBIDO may contain sensitive information
|
|
1738
|
-
throw err;
|
|
1739
|
-
}
|
|
1740
|
-
return data;
|
|
1769
|
+
/**
|
|
1770
|
+
* Applies an output formatter to the resolved ouput promise of a function
|
|
1771
|
+
*
|
|
1772
|
+
* @public
|
|
1773
|
+
*/
|
|
1774
|
+
const usePromiseOutputFormatter = (fn, outputFormatter) => {
|
|
1775
|
+
return async (a) => outputFormatter(await fn(a));
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1778
|
+
const formatInputCurrency = (input) => {
|
|
1779
|
+
const currencyData = CURRENCIES[input];
|
|
1780
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
1781
|
+
if (!currencyData || !currencyData.num) {
|
|
1782
|
+
throw new ValidationError('Unsupported currency', { currency: input });
|
|
1741
1783
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
const value = operationData[field] || operationData[field.toUpperCase()];
|
|
1748
|
-
if (value != null && value) {
|
|
1749
|
-
signedValues.push(value);
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
const signedString = signedValues.join('');
|
|
1753
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing
|
|
1754
|
-
const orderId = operationData.Ds_Order || operationData.DS_ORDER;
|
|
1755
|
-
if (orderId == null || !orderId) {
|
|
1756
|
-
throw new ParseError('Invalid signature');
|
|
1757
|
-
}
|
|
1758
|
-
const signature = operationData.Ds_Signature;
|
|
1759
|
-
const expSignature = sha256Sign(this.secretKey, orderId, signedString);
|
|
1760
|
-
if (signature == null || !signature || signature !== expSignature) {
|
|
1761
|
-
throw new ParseError('Invalid signature');
|
|
1762
|
-
}
|
|
1784
|
+
return currencyData.num;
|
|
1785
|
+
};
|
|
1786
|
+
const formatExpiryDate = ({ expiryYear, expiryMonth }) => {
|
|
1787
|
+
if ((expiryMonth === null || expiryMonth === void 0 ? void 0 : expiryMonth.length) !== 2) {
|
|
1788
|
+
throw new ValidationError('Invalid expiryMonth', { expiryMonth: expiryMonth });
|
|
1763
1789
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
const data = this.processXMLResponse(xml);
|
|
1767
|
-
this.assertXMLPetitionResponseSignature(data);
|
|
1768
|
-
return formatResponse(data.OPERACION);
|
|
1790
|
+
if ((expiryYear === null || expiryYear === void 0 ? void 0 : expiryYear.length) !== 2) {
|
|
1791
|
+
throw new ValidationError('Invalid expiryYear', { expiryYear: expiryYear });
|
|
1769
1792
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1793
|
+
return `${expiryYear}${expiryMonth}`;
|
|
1794
|
+
};
|
|
1795
|
+
/**
|
|
1796
|
+
* Converts a monetary amount from a decimal string currency unit to the atomic currency unit
|
|
1797
|
+
*/
|
|
1798
|
+
const formatAmount = (value, context) => {
|
|
1799
|
+
const { currency } = context;
|
|
1800
|
+
if (!currency) {
|
|
1801
|
+
throw new ValidationError('Missing currency', { currency: currency });
|
|
1773
1802
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1803
|
+
const currencyData = CURRENCIES[currency];
|
|
1804
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
1805
|
+
if (!currencyData || !currencyData.decimals) {
|
|
1806
|
+
throw new ValidationError('Unsupported currency', { currency: currency });
|
|
1778
1807
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
// Promise
|
|
1785
|
-
return await this.wsClient;
|
|
1808
|
+
const rawValue = typeof value === 'number'
|
|
1809
|
+
? Math.round(value * Math.pow(10, currencyData.decimals)).toString()
|
|
1810
|
+
: new Decimal(value).mul(Math.pow(10, currencyData.decimals)).round().toFixed(0);
|
|
1811
|
+
if (rawValue.length > 12) {
|
|
1812
|
+
throw new ValidationError('Amount to charge is too large', { amount: value });
|
|
1786
1813
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
}
|
|
1793
|
-
const res = await client.trataPeticionAsync({ datoEntrada: peticion });
|
|
1794
|
-
const result = res[0].trataPeticionReturn;
|
|
1795
|
-
const parsedResponse = this.processXMLPetitionResponse(result);
|
|
1796
|
-
const resCode = parsedResponse.response;
|
|
1797
|
-
if (resCode != null && resCode > 100 && resCode !== 400 && resCode !== 600) {
|
|
1798
|
-
throw new GatewayError('Error in webservice petition', resCode, res);
|
|
1799
|
-
}
|
|
1800
|
-
return parsedResponse;
|
|
1814
|
+
return rawValue;
|
|
1815
|
+
};
|
|
1816
|
+
const formatLang$1 = (value) => {
|
|
1817
|
+
const langInt = LANGUAGES[value];
|
|
1818
|
+
if (langInt == null) {
|
|
1819
|
+
throw new ValidationError('Invalid lang code', { lang: value });
|
|
1801
1820
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
const signature = msg.Signature;
|
|
1813
|
-
const order = msg.Request.Ds_Order;
|
|
1814
|
-
if (order == null || !order) {
|
|
1815
|
-
throw new ParseError('Invalid notification response');
|
|
1816
|
-
}
|
|
1817
|
-
const expSignature = sha256Sign(this.secretKey, order, signedStr);
|
|
1818
|
-
if (!signature || signature !== expSignature) {
|
|
1819
|
-
throw new ParseError('Invalid signature', signature, xml);
|
|
1820
|
-
}
|
|
1821
|
-
return formatResponse(msg.Request);
|
|
1821
|
+
return langInt;
|
|
1822
|
+
};
|
|
1823
|
+
const baseInputFormatter = (input) => {
|
|
1824
|
+
const { order, merchantCode, transactionType, terminal, amount, currency, merchantName, identifier, group, expiryYear, expiryMonth, pan, cvv, directPayment, merchantData, operationId, productDescription, taxReference, transactionDate, cardHolder, raw, ...unknownInput } = input;
|
|
1825
|
+
const unknownKeys = Object.keys(unknownInput);
|
|
1826
|
+
if (unknownKeys.length > 0) {
|
|
1827
|
+
throw new ValidationError([
|
|
1828
|
+
`Unknown parameters: ${unknownKeys.join(', ')}`,
|
|
1829
|
+
'To pass raw parameters, use "raw"'
|
|
1830
|
+
].join('\n'), unknownInput);
|
|
1822
1831
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1832
|
+
return {
|
|
1833
|
+
DS_MERCHANT_ORDER: order,
|
|
1834
|
+
DS_MERCHANT_MERCHANTCODE: merchantCode,
|
|
1835
|
+
DS_MERCHANT_TRANSACTIONTYPE: transactionType,
|
|
1836
|
+
DS_MERCHANT_TERMINAL: terminal,
|
|
1837
|
+
...(amount != null ? { DS_MERCHANT_AMOUNT: formatAmount(amount, { currency }) } : undefined),
|
|
1838
|
+
...(isStringNotEmpty(currency) ? { DS_MERCHANT_CURRENCY: formatInputCurrency(currency) } : undefined),
|
|
1839
|
+
...(isStringNotEmpty(merchantName) ? { DS_MERCHANT_MERCHANTNAME: merchantName } : undefined),
|
|
1840
|
+
...(isStringNotEmpty(identifier) ? { DS_MERCHANT_IDENTIFIER: identifier } : undefined),
|
|
1841
|
+
...(isStringNotEmpty(group) ? { DS_MERCHANT_GROUP: group } : undefined),
|
|
1842
|
+
...(isStringNotEmpty(expiryYear) || isStringNotEmpty(expiryMonth) ? { DS_MERCHANT_EXPIRYDATE: formatExpiryDate({ expiryYear, expiryMonth }) } : undefined),
|
|
1843
|
+
...(isStringNotEmpty(pan) ? { DS_MERCHANT_PAN: pan } : undefined),
|
|
1844
|
+
...(isStringNotEmpty(cvv) ? { DS_MERCHANT_CVV2: cvv } : undefined),
|
|
1845
|
+
...(isStringNotEmpty(directPayment) ? { DS_MERCHANT_DIRECTPAYMENT: directPayment } : undefined),
|
|
1846
|
+
...(isStringNotEmpty(merchantData) ? { DS_MERCHANT_MERCHANTDATA: merchantData } : undefined),
|
|
1847
|
+
...(isStringNotEmpty(operationId) ? { DS_MERCHANT_IDOPER: operationId } : undefined),
|
|
1848
|
+
...(isStringNotEmpty(productDescription) ? { DS_MERCHANT_PRODUCTDESCRIPTION: productDescription } : undefined),
|
|
1849
|
+
...(isStringNotEmpty(taxReference) ? { DS_MERCHANT_TAX_REFERENCE: taxReference } : undefined),
|
|
1850
|
+
...(isStringNotEmpty(transactionDate) ? { DS_MERCHANT_TRANSACTIONDATE: transactionDate } : undefined),
|
|
1851
|
+
...(isStringNotEmpty(cardHolder) ? { DS_MERCHANT_TITULAR: cardHolder } : undefined),
|
|
1852
|
+
...raw
|
|
1853
|
+
};
|
|
1854
|
+
};
|
|
1855
|
+
/**
|
|
1856
|
+
* Redirection input formatter
|
|
1857
|
+
*
|
|
1858
|
+
* @public
|
|
1859
|
+
*/
|
|
1860
|
+
const redirectInputFormatter = (input) => {
|
|
1861
|
+
const { merchantURL, successURL, errorURL, payMethods, lang, ...baseInput } = input;
|
|
1862
|
+
return {
|
|
1863
|
+
...baseInputFormatter(baseInput),
|
|
1864
|
+
...(isStringNotEmpty(merchantURL) ? { DS_MERCHANT_MERCHANTURL: merchantURL } : undefined),
|
|
1865
|
+
...(isStringNotEmpty(successURL) ? { DS_MERCHANT_URLOK: successURL } : undefined),
|
|
1866
|
+
...(isStringNotEmpty(errorURL) ? { DS_MERCHANT_URLKO: errorURL } : undefined),
|
|
1867
|
+
...(isStringNotEmpty(payMethods) ? { DS_MERCHANT_PAYMETHODS: payMethods } : undefined),
|
|
1868
|
+
...(isStringNotEmpty(lang) ? { DS_MERCHANT_CONSUMERLANGUAGE: formatLang$1(lang) } : undefined),
|
|
1869
|
+
// Overwrite formatted parameters
|
|
1870
|
+
...baseInput.raw
|
|
1871
|
+
};
|
|
1872
|
+
};
|
|
1873
|
+
const requestInputFormatter = (input) => {
|
|
1874
|
+
const { customerMail, customerMobile, smsTemplate, ...baseInput } = input;
|
|
1875
|
+
return {
|
|
1876
|
+
...baseInputFormatter(baseInput),
|
|
1877
|
+
...(isStringNotEmpty(customerMobile) ? { DS_MERCHANT_CUSTOMER_MOBILE: customerMobile } : undefined),
|
|
1878
|
+
...(isStringNotEmpty(customerMail) ? { DS_MERCHANT_CUSTOMER_MAIL: customerMail } : undefined),
|
|
1879
|
+
...(isStringNotEmpty(smsTemplate) ? { DS_MERCHANT_CUSTOMER_SMS_TEXT: smsTemplate } : undefined),
|
|
1880
|
+
// Overwrite formatted parameters
|
|
1881
|
+
...baseInput.raw
|
|
1882
|
+
};
|
|
1883
|
+
};
|
|
1884
|
+
/**
|
|
1885
|
+
* REST iniciaPeticion input formatter
|
|
1886
|
+
*
|
|
1887
|
+
* @public
|
|
1888
|
+
*/
|
|
1889
|
+
const restIniciaPeticionInputFormatter = (raw) => {
|
|
1890
|
+
const { emv3ds, ...requestInput } = raw;
|
|
1891
|
+
return {
|
|
1892
|
+
...requestInputFormatter(requestInput),
|
|
1893
|
+
...(emv3ds != null ? { DS_MERCHANT_EMV3DS: emv3ds } : undefined),
|
|
1894
|
+
// Overwrite formatted parameters
|
|
1895
|
+
...requestInput.raw
|
|
1896
|
+
};
|
|
1897
|
+
};
|
|
1898
|
+
/**
|
|
1899
|
+
* REST trataPeticion input formatter
|
|
1900
|
+
*
|
|
1901
|
+
* @public
|
|
1902
|
+
*/
|
|
1903
|
+
const restTrataPeticionInputFormatter = (raw) => {
|
|
1904
|
+
const { emv3ds, ...requestInput } = raw;
|
|
1905
|
+
return {
|
|
1906
|
+
...requestInputFormatter(requestInput),
|
|
1907
|
+
...(emv3ds != null ? { DS_MERCHANT_EMV3DS: emv3ds } : undefined),
|
|
1908
|
+
// Overwrite formatted parameters
|
|
1909
|
+
...requestInput.raw
|
|
1910
|
+
};
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
dayjs.extend(timezone);
|
|
1914
|
+
dayjs.extend(utc);
|
|
1915
|
+
const formatCountry = (rawCountry) => {
|
|
1916
|
+
const countryInt = Number.parseInt(rawCountry).toString();
|
|
1917
|
+
return REV_COUNTRIES[countryInt];
|
|
1918
|
+
};
|
|
1919
|
+
const formatCardBrand = (rawCardBrand) => {
|
|
1920
|
+
const cardBrandInt = Number.parseInt(rawCardBrand).toString();
|
|
1921
|
+
return REV_CARDBRANDS[cardBrandInt];
|
|
1922
|
+
};
|
|
1923
|
+
const formatResponse = (rawResponse) => Number.parseInt(rawResponse);
|
|
1924
|
+
const baseOutputFormatter = (raw) => {
|
|
1925
|
+
const { Ds_MerchantCode: merchantCode, Ds_Terminal: terminal, Ds_Order: order, Ds_SecurePayment: rawSecurePayment, Ds_TransactionType: transactionType, Ds_Card_Country: rawCardCountry, Ds_Card_Brand: rawCardBrand, Ds_Card_PSD2: rawCardPSD2, Ds_MerchantData: merchantData, Ds_Card_Type: cardType, Ds_AuthorisationCode: authorisationCode } = raw;
|
|
1926
|
+
return {
|
|
1927
|
+
raw,
|
|
1928
|
+
merchantCode,
|
|
1929
|
+
terminal,
|
|
1930
|
+
order,
|
|
1931
|
+
transactionType,
|
|
1932
|
+
...(rawSecurePayment != null && rawSecurePayment.length > 0
|
|
1933
|
+
? { securePayment: rawSecurePayment === '1' || rawSecurePayment === '2' }
|
|
1934
|
+
: undefined),
|
|
1935
|
+
...(isStringNotEmpty(rawCardCountry) ? { cardCountry: formatCountry(rawCardCountry) } : undefined),
|
|
1936
|
+
...(isStringNotEmpty(rawCardBrand) ? { cardBrand: formatCardBrand(rawCardBrand) } : undefined),
|
|
1937
|
+
...(isStringNotEmpty(rawCardPSD2) ? { cardPSD2: rawCardPSD2 === 'Y' } : undefined),
|
|
1938
|
+
...(isStringNotEmpty(merchantData) ? { merchantData } : undefined),
|
|
1939
|
+
...(isStringNotEmpty(cardType) ? { cardType } : undefined),
|
|
1940
|
+
...(isStringNotEmpty(authorisationCode) ? { authorisationCode } : undefined)
|
|
1941
|
+
};
|
|
1942
|
+
};
|
|
1943
|
+
const formatPrice = (params) => {
|
|
1944
|
+
const { Ds_Amount: rawAmount, Ds_Currency: rawCurrency } = params;
|
|
1945
|
+
const currencyInt = Number.parseInt(rawCurrency).toString();
|
|
1946
|
+
const currencyInfo = REV_CURRENCIES[currencyInt];
|
|
1947
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
1948
|
+
if (!currencyInfo) {
|
|
1949
|
+
throw new ParseError('Unknown currency', rawCurrency);
|
|
1828
1950
|
}
|
|
1829
|
-
|
|
1951
|
+
const currency = currencyInfo.code;
|
|
1952
|
+
const amount = new Decimal(rawAmount).dividedBy(Math.pow(10, currencyInfo.decimals)).toFixed();
|
|
1953
|
+
return {
|
|
1954
|
+
amount,
|
|
1955
|
+
currency
|
|
1956
|
+
};
|
|
1957
|
+
};
|
|
1958
|
+
const formatLang = (rawLang) => {
|
|
1959
|
+
const langInt = Number.parseInt(rawLang).toString();
|
|
1960
|
+
return REV_LANGUAGES[langInt];
|
|
1961
|
+
};
|
|
1962
|
+
const notificationOutputFormatter = (raw) => {
|
|
1963
|
+
const { Ds_ConsumerLanguage: rawConsumerLanguage, Ds_Response: rawResponse } = raw;
|
|
1964
|
+
return {
|
|
1965
|
+
...baseOutputFormatter(raw),
|
|
1966
|
+
...formatPrice(raw),
|
|
1967
|
+
response: formatResponse(rawResponse),
|
|
1968
|
+
...(isStringNotEmpty(rawConsumerLanguage) ? { lang: formatLang(rawConsumerLanguage) } : undefined)
|
|
1969
|
+
};
|
|
1970
|
+
};
|
|
1971
|
+
/**
|
|
1972
|
+
* REST notification formatter
|
|
1973
|
+
*
|
|
1974
|
+
* @public
|
|
1975
|
+
*/
|
|
1976
|
+
const restNotificationOutputFormatter = (raw) => {
|
|
1977
|
+
const { Ds_Hour: hour, Ds_Date: rawDate } = raw;
|
|
1978
|
+
const date = rawDate.split('/').reverse().join('-');
|
|
1979
|
+
return {
|
|
1980
|
+
...notificationOutputFormatter(raw),
|
|
1981
|
+
time: hour,
|
|
1982
|
+
date,
|
|
1983
|
+
timestamp: dayjs.tz(`${date}T${hour}`, 'YYYY-MM-DDTHH:mm', 'Europe/Madrid').toDate()
|
|
1984
|
+
};
|
|
1985
|
+
};
|
|
1986
|
+
/**
|
|
1987
|
+
* SOAP notification formatter
|
|
1988
|
+
*
|
|
1989
|
+
* @public
|
|
1990
|
+
*/
|
|
1991
|
+
const soapNotificationOutputFormatter = (raw) => {
|
|
1992
|
+
const { Hora: hour, Fecha: rawDate } = raw;
|
|
1993
|
+
const date = rawDate.split('/').reverse().join('-');
|
|
1994
|
+
return {
|
|
1995
|
+
...notificationOutputFormatter(raw),
|
|
1996
|
+
time: hour,
|
|
1997
|
+
date,
|
|
1998
|
+
timestamp: dayjs.tz(`${date}T${hour}`, 'YYYY-MM-DDTHH:mm', 'Europe/Madrid').toDate()
|
|
1999
|
+
};
|
|
2000
|
+
};
|
|
2001
|
+
const requestOutputFormatter = (raw) => {
|
|
2002
|
+
const { Ds_CardNumber: deprecatedCardNumber, Ds_Card_Number: newCardNumber, Ds_Merchant_Identifier: identifier, Ds_ExpiryDate: rawExpiryDate, Ds_UrlPago2Fases: payURL, Ds_Language: rawLang } = raw;
|
|
2003
|
+
const cardNumber = newCardNumber !== null && newCardNumber !== void 0 ? newCardNumber : deprecatedCardNumber;
|
|
2004
|
+
return {
|
|
2005
|
+
...baseOutputFormatter(raw),
|
|
2006
|
+
...(isStringNotEmpty(cardNumber) ? { cardNumber } : undefined),
|
|
2007
|
+
...(isStringNotEmpty(identifier) ? { identifier } : undefined),
|
|
2008
|
+
...(isStringNotEmpty(payURL) ? { payURL } : undefined),
|
|
2009
|
+
...(isStringNotEmpty(rawExpiryDate)
|
|
2010
|
+
? { expiryYear: rawExpiryDate.slice(0, 2), expiryMonth: rawExpiryDate.slice(2, 4) }
|
|
2011
|
+
: undefined),
|
|
2012
|
+
...(isStringNotEmpty(rawLang) ? { lang: formatLang(rawLang) } : undefined)
|
|
2013
|
+
};
|
|
2014
|
+
};
|
|
2015
|
+
/**
|
|
2016
|
+
* REST iniciaPeticion output formatter
|
|
2017
|
+
*
|
|
2018
|
+
* @public
|
|
2019
|
+
*/
|
|
2020
|
+
const restIniciaPeticionOutputFormatter = (raw) => {
|
|
2021
|
+
const { Ds_EMV3DS: emv3ds } = raw;
|
|
2022
|
+
return {
|
|
2023
|
+
...requestOutputFormatter(raw),
|
|
2024
|
+
...(emv3ds != null ? { emv3ds } : undefined)
|
|
2025
|
+
};
|
|
2026
|
+
};
|
|
2027
|
+
/**
|
|
2028
|
+
* REST trataPeticion output formatter
|
|
2029
|
+
*
|
|
2030
|
+
* @public
|
|
2031
|
+
*/
|
|
2032
|
+
const restTrataPeticionOutputFormatter = (raw) => {
|
|
2033
|
+
const { Ds_Response: rawResponse, Ds_EMV3DS: emv3ds } = raw;
|
|
2034
|
+
return {
|
|
2035
|
+
...requestOutputFormatter(raw),
|
|
2036
|
+
...formatPrice(raw),
|
|
2037
|
+
...(emv3ds != null ? { emv3ds } : undefined),
|
|
2038
|
+
...(rawResponse != null ? { response: Number.parseInt(rawResponse) } : undefined)
|
|
2039
|
+
};
|
|
2040
|
+
};
|
|
1830
2041
|
|
|
1831
|
-
export { PRODUCTION_URLS,
|
|
2042
|
+
export { CARDBRANDS, COUNTRIES, CURRENCIES, GatewayError, HTTPError, LANGUAGES, PRODUCTION_URLS, ParseError, REV_CARDBRANDS, REV_COUNTRIES, REV_CURRENCIES, REV_LANGUAGES, RedsysError, ResponseError, SANDBOX_URLS, TRANSACTION_TYPES, ValidationError, assertSuccessfulResponseCode, create3DSMethodForm, create3DSv1ChallengeForm, create3DSv2ChallengeForm, createRedsysAPI, deserializeCres, deserializeThreeDSMethodData, detectSoapVersion, getResponseCodeMessage, getSISErrorCodeMessage, isResponseCodeOk, mimicSoap11NotificationResponse, mimicSoap12NotificationResponse, mimicSoapNotificationReceiver, randomTransactionId, redirectInputFormatter, restIniciaPeticionInputFormatter, restIniciaPeticionOutputFormatter, restNotificationOutputFormatter, restTrataPeticionInputFormatter, restTrataPeticionOutputFormatter, soapNotificationOutputFormatter, useOutputFormatter, usePromiseOutputFormatter, useSingleInputFormatter };
|