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/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
- const TRANSACTION_TYPES = {
494
- AUTHORIZATION: '0',
495
- PRE_AUTHORIZATION: '1',
496
- CONFIRMATION: '2',
497
- AUTO_REFUND: '3',
498
- RECURRING_TRANSACTION: '5',
499
- SUCCESSIVE_TRANSACTION: '6',
500
- PRE_AUTHENTICATION: '7',
501
- PRE_AUTHENTICATION_CONFIRMATION: '8',
502
- PRE_AUTHORIZATION_CANCEL: '9',
503
- DEFERRED_AUTHORIZATION: 'O',
504
- PAYLINK: 'F',
505
- DEFERRED_AUTHORIZATION_CONFIRMATION: 'P',
506
- DEFERRED_AUTHORIZATION_CANCEL: 'Q',
507
- DEFERRED_INITIAL_FEE: 'R',
508
- DEFERRED_SUCCESSIVE_FEE: 'S',
509
- NO_AUTHENTICATION: 'A',
510
- DELETE_REFERENCE: '44' // Eliminar referencia
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 {buffer} buf - Input buffer
519
- * @params {number} blocksize - Size of block
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 {string} key - Key to encrypt message
530
- * @params {string} message - Message to be encrypted
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 getResponseCodeMessage = (code) => {
551
- if ((typeof code !== 'number' && typeof code !== 'string') || !Number.isInteger(code)) {
552
- return null;
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 lookupNum = typeof code === 'string' ? Number.parseInt(code.trim()) : code;
555
- if (!Number.isFinite(lookupNum) || lookupNum < 0) {
556
- return null;
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
- const msg = RESPONSE_CODES[lookupNum.toString()];
559
- if ((msg == null || !msg) && lookupNum < 100) {
560
- return 'Transacción autorizada para pagos y preautorizaciones';
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 msg !== null && msg !== void 0 ? msg : null;
692
+ return order;
563
693
  };
564
- const getSISErrorCodeMessage = (code) => {
565
- if (!code || typeof code !== 'string') {
566
- return undefined;
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
- return SIS_ERROR_CODES[code.trim()];
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
- // Order is important
571
- const signedFieldsXMLResponse = ['Ds_Amount', 'Ds_Order',
572
- 'Ds_MerchantCode', 'Ds_Currency', 'Ds_Response', 'Ds_CardNumber',
573
- 'Ds_TransactionType', 'Ds_SecurePayment'];
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
- const mimicSoap12NotificationResponse = (answer) => {
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
- // ISO 639-1
675
- const LANGUAGES = {
676
- es: '1',
677
- en: '2',
678
- ca: '3',
679
- fr: '4',
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
- // ISO 3166 Alpha 2 => (UN M49 === ISO 3166 Num)
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
- const isStringNotEmpty = (str) => str != null && str.length > 0;
1194
- const inputParamsFormatter = (input, options) => {
1195
- const obj = {};
1196
- for (const key of Object.keys(input)) {
1197
- switch (key) {
1198
- case 'expiryMonth':
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
- dayjs.extend(timezone);
1541
- dayjs.extend(utc);
1542
- const hourFormatter = (obj, value) => {
1543
- if (value)
1544
- obj.hour = value;
1545
- };
1546
- const dateFormatter = (obj, value) => {
1547
- // Transform to ISO format
1548
- if (value)
1549
- obj.date = value.split('/').reverse().join('-');
1550
- };
1551
- const langFormatter = (obj, value) => {
1552
- const langInt = Number.parseInt(value);
1553
- const lang = REV_LANGUAGES[langInt];
1554
- if (lang != null && lang)
1555
- obj.lang = lang;
1556
- };
1557
- const responseFormatters = {
1558
- DS_HOUR: hourFormatter,
1559
- HORA: hourFormatter,
1560
- DS_DATE: dateFormatter,
1561
- FECHA: dateFormatter,
1562
- DS_CURRENCY: (obj, value) => {
1563
- const currencyInt = Number.parseInt(value);
1564
- const currency = REV_CURRENCIES[currencyInt];
1565
- if (currency)
1566
- obj.currency = currency.code;
1567
- },
1568
- DS_AMOUNT: (obj, value) => {
1569
- if (value)
1570
- obj.amount = Number.parseInt(value);
1571
- },
1572
- DS_RESPONSE: (obj, value) => {
1573
- // Remove leading zeros
1574
- if (value)
1575
- obj.response = Number.parseInt(value);
1576
- },
1577
- DS_ORDER: (obj, value) => {
1578
- obj.order = value;
1579
- },
1580
- DS_MERCHANTCODE: (obj, value) => {
1581
- obj.merchantCode = value;
1582
- },
1583
- DS_TERMINAL: (obj, value) => {
1584
- obj.terminal = value;
1585
- },
1586
- DS_MERCHANTDATA: (obj, value) => {
1587
- if (value)
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
- const js2xml = new j2xParser({});
1654
- const PRODUCTION_URLS = {
1655
- redirect: 'https://sis.redsys.es/sis/realizarPago',
1656
- ws: 'https://sis.redsys.es/sis/services/SerClsWSEntrada/wsdl/SerClsWSEntrada.wsdl'
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
- const SANDBOX_URLS = {
1659
- redirect: 'https://sis-t.redsys.es:25443/sis/realizarPago',
1660
- ws: 'https://sis-t.redsys.es:25443/sis/services/SerClsWSEntrada/wsdl/SerClsWSEntrada.wsdl'
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
- class Redsys {
1663
- constructor(options) {
1664
- if (!options.secretKey || typeof options.secretKey !== 'string') {
1665
- throw new RedsysError('A secretKey key must be provided');
1666
- }
1667
- if (typeof options.urls !== 'object' || options.urls == null ||
1668
- !options.urls.ws || !options.urls.redirect) {
1669
- throw new RedsysError('URLs must be provided');
1670
- }
1671
- this.secretKey = options.secretKey;
1672
- this.urls = options.urls;
1673
- }
1674
- signOrderText(order, text) {
1675
- return sha256Sign(this.secretKey, order, text);
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
- assertXMLPetitionResponseSignature(data) {
1743
- const operationData = data.OPERACION;
1744
- const signedValues = [];
1745
- for (const field of signedFieldsXMLResponse) {
1746
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing
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
- // Processes Web Service Petitions Responses
1765
- processXMLPetitionResponse(xml) {
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
- xmlPetitionParameters(paramsInput, options) {
1771
- const datosEntrada = { DATOSENTRADA: formatParams(paramsInput, options) };
1772
- return js2xml.parse(datosEntrada);
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
- xmlPetitionSignedData(paramsInput, options) {
1775
- const datosEntradaXML = this.xmlPetitionParameters(paramsInput, options);
1776
- const signature = sha256Sign(this.secretKey, paramsInput.order, datosEntradaXML);
1777
- return `<REQUEST>${datosEntradaXML}<DS_SIGNATUREVERSION>HMAC_SHA256_V1</DS_SIGNATUREVERSION><DS_SIGNATURE>${signature}</DS_SIGNATURE></REQUEST>`;
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
- // Good idea unless sessions are used
1780
- async getWSClient() {
1781
- if (!this.wsClient) {
1782
- this.wsClient = createClientAsync(this.urls.ws, {});
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
- async wsPetition(paramsInput, options) {
1788
- const peticion = this.xmlPetitionSignedData(paramsInput, options);
1789
- const client = await this.getWSClient();
1790
- if (!client.trataPeticionAsync) {
1791
- throw new GatewayError('Cannot obtain client');
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
- processSoapNotification(xml) {
1803
- const startToken = '<Request';
1804
- const endToken = '</Request>';
1805
- const startPos = xml.indexOf(startToken);
1806
- const endPos = xml.indexOf(endToken);
1807
- if (startPos < 0 || endPos < 0 || startPos > endPos) {
1808
- throw new ParseError('Cannot find payload of SOAP notification Request', xml);
1809
- }
1810
- const signedStr = xml.slice(startPos, endPos + endToken.length);
1811
- const msg = xmlParser.parse(xml, { parseNodeValue: false }).Message;
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
- soapNotificationAnswer(order, bool) {
1824
- const answer = bool ? 'OK' : 'KO';
1825
- const response = `<Response Ds_Version="0.0"><Ds_Response_Merchant>${answer}</Ds_Response_Merchant></Response>`;
1826
- const signature = sha256Sign(this.secretKey, order, response);
1827
- return `<Message>${response}<Signature>${signature}</Signature></Message>`;
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, Redsys, SANDBOX_URLS, TRANSACTION_TYPES, detectSoapVersion, getResponseCodeMessage, getSISErrorCodeMessage, mimicSoap11NotificationResponse, mimicSoap12NotificationResponse, mimicSoapNotificationReceiver, randomTransactionId };
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 };