@networksolution/sia-vpos-node 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -10
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +174 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +76 -1
- package/dist/types.js.map +1 -1
- package/dist/vpos-client.d.ts +114 -1
- package/dist/vpos-client.d.ts.map +1 -1
- package/dist/vpos-client.js +478 -8
- package/dist/vpos-client.js.map +1 -1
- package/dist/xml.d.ts +74 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +55 -0
- package/dist/xml.js.map +1 -1
- package/package.json +1 -1
package/dist/vpos-client.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.VposClient = void 0;
|
|
4
7
|
exports.generateTimestamp = generateTimestamp;
|
|
5
8
|
exports.generateReqRefNum = generateReqRefNum;
|
|
9
|
+
const crypto_js_1 = __importDefault(require("crypto-js"));
|
|
6
10
|
const mac_1 = require("./mac");
|
|
7
11
|
const xml_1 = require("./xml");
|
|
8
12
|
// ─── Defaults ────────────────────────────────────────────────────────────────
|
|
@@ -10,20 +14,31 @@ const API_URLS = {
|
|
|
10
14
|
production: 'https://virtualpos.sia.eu/vpos/apibo/apiBOXML-UTF8.app',
|
|
11
15
|
test: 'https://virtualpostest.sia.eu/vpos/apibo/apiBOXML-UTF8.app',
|
|
12
16
|
};
|
|
17
|
+
const REDIRECT_URLS = {
|
|
18
|
+
production: 'https://virtualpos.sia.eu/vpos/payments/main',
|
|
19
|
+
test: 'https://virtualpostest.sia.eu/vpos/payments/main',
|
|
20
|
+
};
|
|
13
21
|
const DEFAULT_RELEASE = '02';
|
|
14
22
|
const DEFAULT_CURRENCY = '978'; // EUR
|
|
15
23
|
const DEFAULT_EXPONENT = '2';
|
|
24
|
+
function resolveEnvironment(config) {
|
|
25
|
+
return config.environment
|
|
26
|
+
|| (typeof process !== 'undefined' && process.env?.VPOS_ENV)
|
|
27
|
+
|| 'test';
|
|
28
|
+
}
|
|
16
29
|
/**
|
|
17
|
-
* Resolve the
|
|
30
|
+
* Resolve the API URL from config or VPOS_ENV env variable.
|
|
18
31
|
* Priority: config.apiUrl > config.environment > VPOS_ENV > 'test' (default)
|
|
19
32
|
*/
|
|
20
33
|
function resolveApiUrl(config) {
|
|
21
34
|
if (config.apiUrl)
|
|
22
35
|
return config.apiUrl;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
return API_URLS[resolveEnvironment(config)] || API_URLS.test;
|
|
37
|
+
}
|
|
38
|
+
function resolveRedirectUrl(config) {
|
|
39
|
+
if (config.redirectUrl)
|
|
40
|
+
return config.redirectUrl;
|
|
41
|
+
return REDIRECT_URLS[resolveEnvironment(config)] || REDIRECT_URLS.test;
|
|
27
42
|
}
|
|
28
43
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
29
44
|
/**
|
|
@@ -56,15 +71,16 @@ function generateReqRefNum(date) {
|
|
|
56
71
|
// ─── Main Client ─────────────────────────────────────────────────────────────
|
|
57
72
|
class VposClient {
|
|
58
73
|
constructor(config) {
|
|
59
|
-
const environment = config
|
|
60
|
-
|| (typeof process !== 'undefined' && process.env?.VPOS_ENV)
|
|
61
|
-
|| 'test';
|
|
74
|
+
const environment = resolveEnvironment(config);
|
|
62
75
|
this.config = {
|
|
63
76
|
shopId: config.shopId,
|
|
64
77
|
operatorId: config.operatorId,
|
|
65
78
|
secretKey: config.secretKey,
|
|
79
|
+
startKey: config.startKey || config.secretKey,
|
|
80
|
+
apiResultKey: config.apiResultKey || config.secretKey,
|
|
66
81
|
hashAlgorithm: config.hashAlgorithm || 'hmac-sha256',
|
|
67
82
|
apiUrl: resolveApiUrl(config),
|
|
83
|
+
redirectUrl: resolveRedirectUrl(config),
|
|
68
84
|
environment,
|
|
69
85
|
release: config.release || DEFAULT_RELEASE,
|
|
70
86
|
};
|
|
@@ -81,6 +97,10 @@ class VposClient {
|
|
|
81
97
|
get apiUrl() {
|
|
82
98
|
return this.config.apiUrl;
|
|
83
99
|
}
|
|
100
|
+
/** Returns the current Redirect URL */
|
|
101
|
+
get redirectUrl() {
|
|
102
|
+
return this.config.redirectUrl;
|
|
103
|
+
}
|
|
84
104
|
// ── Internal: send XML request to VPOS ──────────────────────────────────────
|
|
85
105
|
async sendRequest(xml) {
|
|
86
106
|
const body = `data=${encodeURIComponent(xml)}`;
|
|
@@ -525,6 +545,104 @@ class VposClient {
|
|
|
525
545
|
};
|
|
526
546
|
}
|
|
527
547
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
548
|
+
// LISTOPERATION (Consultation: list operations by date range)
|
|
549
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
550
|
+
async listOperations(data) {
|
|
551
|
+
const timestamp = generateTimestamp();
|
|
552
|
+
const reqRefNum = generateReqRefNum();
|
|
553
|
+
// MAC fields per spec 4.2.4
|
|
554
|
+
const macFields = [
|
|
555
|
+
['OPERATION', 'LISTOPERATION'],
|
|
556
|
+
['TIMESTAMP', timestamp],
|
|
557
|
+
['SHOPID', this.config.shopId],
|
|
558
|
+
['OPERATORID', this.config.operatorId],
|
|
559
|
+
['REQREFNUM', reqRefNum],
|
|
560
|
+
['STARTDATE', data.startDate],
|
|
561
|
+
['ENDDATE', data.endDate],
|
|
562
|
+
['OPDESCR', data.opDescr],
|
|
563
|
+
['OPTIONS', data.options],
|
|
564
|
+
];
|
|
565
|
+
const mac = (0, mac_1.generateMAC)(macFields, this.config.secretKey, this.config.hashAlgorithm);
|
|
566
|
+
const dataXml = [
|
|
567
|
+
'<ListOperation>',
|
|
568
|
+
(0, xml_1.buildHeaderXml)(this.config.shopId, this.config.operatorId, reqRefNum),
|
|
569
|
+
(0, xml_1.el)('StartDate', data.startDate),
|
|
570
|
+
(0, xml_1.el)('EndDate', data.endDate),
|
|
571
|
+
(0, xml_1.el)('SrcType', data.srcType),
|
|
572
|
+
(0, xml_1.el)('OpDescr', data.opDescr),
|
|
573
|
+
'</ListOperation>',
|
|
574
|
+
].filter(Boolean).join('\n');
|
|
575
|
+
const xml = (0, xml_1.buildBPWXmlRequest)({
|
|
576
|
+
release: this.config.release,
|
|
577
|
+
operation: 'LISTOPERATION',
|
|
578
|
+
timestamp,
|
|
579
|
+
mac,
|
|
580
|
+
dataXml,
|
|
581
|
+
});
|
|
582
|
+
const responseXml = await this.sendRequest(xml);
|
|
583
|
+
const parsed = (0, xml_1.parseBPWXmlResponse)(responseXml);
|
|
584
|
+
return {
|
|
585
|
+
...parsed,
|
|
586
|
+
numberOfItems: parsed.data ? (0, xml_1.getXmlValue)(parsed.data, 'NumberOfItems') : undefined,
|
|
587
|
+
operations: parsed.data ? (0, xml_1.parseOperationList)(parsed.data) : [],
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
591
|
+
// LISTAUTHORIZATION (Consultation: list authorizations by filter)
|
|
592
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
593
|
+
async listAuthorizations(data) {
|
|
594
|
+
const timestamp = generateTimestamp();
|
|
595
|
+
const reqRefNum = generateReqRefNum();
|
|
596
|
+
// MAC fields per spec 4.2.5 — TRANSACTIONID must ALWAYS be included (even empty)
|
|
597
|
+
// STARTTIME/ENDTIME only included when searching by time
|
|
598
|
+
const macParts = [
|
|
599
|
+
`OPERATION=LISTAUTHORIZATION`,
|
|
600
|
+
`TIMESTAMP=${timestamp}`,
|
|
601
|
+
`SHOPID=${this.config.shopId}`,
|
|
602
|
+
`OPERATORID=${this.config.operatorId}`,
|
|
603
|
+
`REQREFNUM=${reqRefNum}`,
|
|
604
|
+
];
|
|
605
|
+
if (data.startDate)
|
|
606
|
+
macParts.push(`STARTDATE=${data.startDate}`);
|
|
607
|
+
if (data.endDate)
|
|
608
|
+
macParts.push(`ENDDATE=${data.endDate}`);
|
|
609
|
+
macParts.push(`FILTER=${data.filter}`);
|
|
610
|
+
macParts.push(`TRANSACTIONID=${data.transactionId || ''}`);
|
|
611
|
+
if (data.startTime)
|
|
612
|
+
macParts.push(`STARTTIME=${data.startTime}`);
|
|
613
|
+
if (data.endTime)
|
|
614
|
+
macParts.push(`ENDTIME=${data.endTime}`);
|
|
615
|
+
if (data.options)
|
|
616
|
+
macParts.push(`OPTIONS=${data.options}`);
|
|
617
|
+
const macString = macParts.join('&');
|
|
618
|
+
const mac = (0, mac_1.computeHash)(macString, this.config.secretKey, this.config.hashAlgorithm);
|
|
619
|
+
const dataXml = [
|
|
620
|
+
'<ListAuthorization>',
|
|
621
|
+
(0, xml_1.buildHeaderXml)(this.config.shopId, this.config.operatorId, reqRefNum),
|
|
622
|
+
(0, xml_1.el)('StartDate', data.startDate),
|
|
623
|
+
(0, xml_1.el)('EndDate', data.endDate),
|
|
624
|
+
(0, xml_1.el)('Filter', data.filter),
|
|
625
|
+
(0, xml_1.el)('TransactionID', data.transactionId),
|
|
626
|
+
(0, xml_1.el)('StartTime', data.startTime),
|
|
627
|
+
(0, xml_1.el)('EndTime', data.endTime),
|
|
628
|
+
'</ListAuthorization>',
|
|
629
|
+
].filter(Boolean).join('\n');
|
|
630
|
+
const xml = (0, xml_1.buildBPWXmlRequest)({
|
|
631
|
+
release: this.config.release,
|
|
632
|
+
operation: 'LISTAUTHORIZATION',
|
|
633
|
+
timestamp,
|
|
634
|
+
mac,
|
|
635
|
+
dataXml,
|
|
636
|
+
});
|
|
637
|
+
const responseXml = await this.sendRequest(xml);
|
|
638
|
+
const parsed = (0, xml_1.parseBPWXmlResponse)(responseXml);
|
|
639
|
+
return {
|
|
640
|
+
...parsed,
|
|
641
|
+
numberOfItems: parsed.data ? (0, xml_1.getXmlValue)(parsed.data, 'NumberOfItems') : undefined,
|
|
642
|
+
authorizations: parsed.data ? (0, xml_1.parseAuthorizationList)(parsed.data) : [],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
528
646
|
// WEBHOOK VERIFICATION
|
|
529
647
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
530
648
|
/**
|
|
@@ -603,6 +721,358 @@ class VposClient {
|
|
|
603
721
|
linkCreated: parsed.data ? (0, xml_1.parseLinkCreated)(parsed.data) : undefined,
|
|
604
722
|
};
|
|
605
723
|
}
|
|
724
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
725
|
+
// REDIRECT: Build Payment Form (4.2.1)
|
|
726
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
727
|
+
/**
|
|
728
|
+
* Build redirect form data for standard payment initiation.
|
|
729
|
+
* Returns URL, form fields, and pre-built HTML.
|
|
730
|
+
* The customer's browser should POST this form to SIA VPOS.
|
|
731
|
+
*/
|
|
732
|
+
buildRedirectForm(data) {
|
|
733
|
+
const exponent = data.exponent || DEFAULT_EXPONENT;
|
|
734
|
+
// MAC fields per spec 5.2.1 — order is critical!
|
|
735
|
+
const macFields = [
|
|
736
|
+
['URLMS', data.urlMs],
|
|
737
|
+
['URLDONE', data.urlDone],
|
|
738
|
+
['ORDERID', data.orderId],
|
|
739
|
+
['SHOPID', this.config.shopId],
|
|
740
|
+
['AMOUNT', data.amount],
|
|
741
|
+
['CURRENCY', data.currency],
|
|
742
|
+
['EXPONENT', data.exponent], // only if present
|
|
743
|
+
['ACCOUNTINGMODE', data.accountingMode],
|
|
744
|
+
['AUTHORMODE', data.authorMode],
|
|
745
|
+
['OPTIONS', data.options],
|
|
746
|
+
['NAME', data.name],
|
|
747
|
+
['SURNAME', data.surname],
|
|
748
|
+
['TAXID', data.taxId],
|
|
749
|
+
['LOCKCARD', data.lockCard],
|
|
750
|
+
['COMMIS', data.commis],
|
|
751
|
+
['ORDDESCR', data.ordDescr],
|
|
752
|
+
['VSID', data.vsid],
|
|
753
|
+
['OPDESCR', data.opDescr],
|
|
754
|
+
['REMAININGDURATION', data.remainingDuration],
|
|
755
|
+
['USERID', data.userId],
|
|
756
|
+
['PHONENUMBER', data.phoneNumber],
|
|
757
|
+
['CAUSATION', data.causation],
|
|
758
|
+
['USER', data.user],
|
|
759
|
+
['PRODUCTREF', data.productRef],
|
|
760
|
+
['ANTIFRAUD', data.antifraud],
|
|
761
|
+
['3DSDATA', data.threeDsData],
|
|
762
|
+
['TRECURR', data.tRecurr],
|
|
763
|
+
['URLMSHEADER', data.urlMsHeader],
|
|
764
|
+
['INSTALLMENTSNUMBER', data.installmentsNumber],
|
|
765
|
+
['TICKLERPLAN', data.ticklerPlan],
|
|
766
|
+
];
|
|
767
|
+
const mac = (0, mac_1.generateMAC)(macFields, this.config.startKey, this.config.hashAlgorithm);
|
|
768
|
+
// Build form fields (all uppercase, case sensitive per spec)
|
|
769
|
+
const fields = {
|
|
770
|
+
PAGE: 'LAND',
|
|
771
|
+
AMOUNT: String(data.amount),
|
|
772
|
+
CURRENCY: data.currency,
|
|
773
|
+
ORDERID: data.orderId,
|
|
774
|
+
SHOPID: this.config.shopId,
|
|
775
|
+
URLBACK: data.urlBack,
|
|
776
|
+
URLDONE: data.urlDone,
|
|
777
|
+
URLMS: data.urlMs,
|
|
778
|
+
ACCOUNTINGMODE: data.accountingMode,
|
|
779
|
+
AUTHORMODE: data.authorMode,
|
|
780
|
+
MAC: mac,
|
|
781
|
+
};
|
|
782
|
+
// Add optional fields
|
|
783
|
+
const optionals = [
|
|
784
|
+
['URLMSHEADER', data.urlMsHeader],
|
|
785
|
+
['LANG', data.lang],
|
|
786
|
+
['SHOPEMAIL', data.shopEmail],
|
|
787
|
+
['OPTIONS', data.options],
|
|
788
|
+
['LOCKCARD', data.lockCard],
|
|
789
|
+
['EMAIL', data.email],
|
|
790
|
+
['ORDDESCR', data.ordDescr],
|
|
791
|
+
['VSID', data.vsid],
|
|
792
|
+
['OPDESCR', data.opDescr],
|
|
793
|
+
['REMAININGDURATION', data.remainingDuration],
|
|
794
|
+
['USERID', data.userId],
|
|
795
|
+
['PHONENUMBER', data.phoneNumber],
|
|
796
|
+
['CAUSATION', data.causation],
|
|
797
|
+
['USER', data.user],
|
|
798
|
+
['NAME', data.name],
|
|
799
|
+
['SURNAME', data.surname],
|
|
800
|
+
['TAXID', data.taxId],
|
|
801
|
+
['PRODUCTREF', data.productRef],
|
|
802
|
+
['ANTIFRAUD', data.antifraud],
|
|
803
|
+
['3DSDATA', data.threeDsData],
|
|
804
|
+
['TRECURR', data.tRecurr],
|
|
805
|
+
['INSTALLMENTSNUMBER', data.installmentsNumber],
|
|
806
|
+
['TICKLERPLAN', data.ticklerPlan],
|
|
807
|
+
['EXPONENT', data.exponent],
|
|
808
|
+
['COMMIS', data.commis],
|
|
809
|
+
];
|
|
810
|
+
for (const [key, value] of optionals) {
|
|
811
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
812
|
+
fields[key] = String(value);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const url = this.config.redirectUrl;
|
|
816
|
+
const html = buildHtmlForm(url, fields);
|
|
817
|
+
return { url, fields, html };
|
|
818
|
+
}
|
|
819
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
820
|
+
// REDIRECT: Build Token Payment Form (4.2.4)
|
|
821
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
822
|
+
/**
|
|
823
|
+
* Build redirect form data for token-based payment (saved card / pan alias).
|
|
824
|
+
* Returns URL, form fields, and pre-built HTML.
|
|
825
|
+
*/
|
|
826
|
+
buildTokenRedirectForm(data) {
|
|
827
|
+
// MAC fields per spec 5.2.3 — same as 5.2.1 but adds TOKEN, EXPDATE, NETWORK, IBAN at end
|
|
828
|
+
const macFields = [
|
|
829
|
+
['URLMS', data.urlMs],
|
|
830
|
+
['URLDONE', data.urlDone],
|
|
831
|
+
['ORDERID', data.orderId],
|
|
832
|
+
['SHOPID', this.config.shopId],
|
|
833
|
+
['AMOUNT', data.amount],
|
|
834
|
+
['CURRENCY', data.currency],
|
|
835
|
+
['EXPONENT', data.exponent],
|
|
836
|
+
['ACCOUNTINGMODE', data.accountingMode],
|
|
837
|
+
['AUTHORMODE', data.authorMode],
|
|
838
|
+
['OPTIONS', data.options],
|
|
839
|
+
['NAME', data.name],
|
|
840
|
+
['SURNAME', data.surname],
|
|
841
|
+
['TAXID', data.taxId],
|
|
842
|
+
['LOCKCARD', data.lockCard],
|
|
843
|
+
['COMMIS', data.commis],
|
|
844
|
+
['ORDDESCR', data.ordDescr],
|
|
845
|
+
['VSID', data.vsid],
|
|
846
|
+
['OPDESCR', data.opDescr],
|
|
847
|
+
['REMAININGDURATION', data.remainingDuration],
|
|
848
|
+
['USERID', data.userId],
|
|
849
|
+
['PHONENUMBER', data.phoneNumber],
|
|
850
|
+
['CAUSATION', data.causation],
|
|
851
|
+
['USER', data.user],
|
|
852
|
+
['PRODUCTREF', data.productRef],
|
|
853
|
+
['ANTIFRAUD', data.antifraud],
|
|
854
|
+
['3DSDATA', data.threeDsData],
|
|
855
|
+
['TRECURR', data.tRecurr],
|
|
856
|
+
['CRECURR', data.cRecurr],
|
|
857
|
+
['URLMSHEADER', data.urlMsHeader],
|
|
858
|
+
['INSTALLMENTSNUMBER', data.installmentsNumber],
|
|
859
|
+
['TICKLERPLAN', data.ticklerPlan],
|
|
860
|
+
['TOKEN', data.token],
|
|
861
|
+
['EXPDATE', data.expDate],
|
|
862
|
+
['NETWORK', data.network],
|
|
863
|
+
['IBAN', data.iban],
|
|
864
|
+
];
|
|
865
|
+
const mac = (0, mac_1.generateMAC)(macFields, this.config.startKey, this.config.hashAlgorithm);
|
|
866
|
+
const fields = {
|
|
867
|
+
PAGE: 'TOKEN',
|
|
868
|
+
AMOUNT: String(data.amount),
|
|
869
|
+
CURRENCY: data.currency,
|
|
870
|
+
ORDERID: data.orderId,
|
|
871
|
+
SHOPID: this.config.shopId,
|
|
872
|
+
URLBACK: data.urlBack,
|
|
873
|
+
URLDONE: data.urlDone,
|
|
874
|
+
URLMS: data.urlMs,
|
|
875
|
+
ACCOUNTINGMODE: data.accountingMode,
|
|
876
|
+
AUTHORMODE: data.authorMode,
|
|
877
|
+
TOKEN: data.token,
|
|
878
|
+
NETWORK: data.network,
|
|
879
|
+
TRECURR: data.tRecurr || 'C',
|
|
880
|
+
MAC: mac,
|
|
881
|
+
};
|
|
882
|
+
// Optional fields
|
|
883
|
+
const optionals = [
|
|
884
|
+
['URLMSHEADER', data.urlMsHeader],
|
|
885
|
+
['LANG', data.lang],
|
|
886
|
+
['SHOPEMAIL', data.shopEmail],
|
|
887
|
+
['OPTIONS', data.options],
|
|
888
|
+
['EMAIL', data.email],
|
|
889
|
+
['ORDDESCR', data.ordDescr],
|
|
890
|
+
['VSID', data.vsid],
|
|
891
|
+
['OPDESCR', data.opDescr],
|
|
892
|
+
['REMAININGDURATION', data.remainingDuration],
|
|
893
|
+
['USERID', data.userId],
|
|
894
|
+
['PHONENUMBER', data.phoneNumber],
|
|
895
|
+
['CAUSATION', data.causation],
|
|
896
|
+
['USER', data.user],
|
|
897
|
+
['NAME', data.name],
|
|
898
|
+
['SURNAME', data.surname],
|
|
899
|
+
['TAXID', data.taxId],
|
|
900
|
+
['PRODUCTREF', data.productRef],
|
|
901
|
+
['ANTIFRAUD', data.antifraud],
|
|
902
|
+
['3DSDATA', data.threeDsData],
|
|
903
|
+
['INSTALLMENTSNUMBER', data.installmentsNumber],
|
|
904
|
+
['TICKLERPLAN', data.ticklerPlan],
|
|
905
|
+
['EXPONENT', data.exponent],
|
|
906
|
+
['COMMIS', data.commis],
|
|
907
|
+
['EXPDATE', data.expDate],
|
|
908
|
+
['CRECURR', data.cRecurr],
|
|
909
|
+
['IBAN', data.iban],
|
|
910
|
+
];
|
|
911
|
+
for (const [key, value] of optionals) {
|
|
912
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
913
|
+
fields[key] = String(value);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const url = this.config.redirectUrl;
|
|
917
|
+
const html = buildHtmlForm(url, fields);
|
|
918
|
+
return { url, fields, html };
|
|
919
|
+
}
|
|
920
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
921
|
+
// REDIRECT: Parse Outcome (4.2.3)
|
|
922
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
923
|
+
/**
|
|
924
|
+
* Parse URLMS or URLDONE query string parameters into a typed object.
|
|
925
|
+
* Accepts a full URL string, a query string, or URLSearchParams.
|
|
926
|
+
*/
|
|
927
|
+
parseOutcomeParams(input) {
|
|
928
|
+
let params;
|
|
929
|
+
if (typeof input === 'string') {
|
|
930
|
+
const qIdx = input.indexOf('?');
|
|
931
|
+
params = new URLSearchParams(qIdx >= 0 ? input.substring(qIdx + 1) : input);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
params = input;
|
|
935
|
+
}
|
|
936
|
+
const g = (key) => params.get(key) || '';
|
|
937
|
+
return {
|
|
938
|
+
orderId: g('ORDERID'),
|
|
939
|
+
shopId: g('SHOPID'),
|
|
940
|
+
authNumber: g('AUTHNUMBER'),
|
|
941
|
+
amount: g('AMOUNT'),
|
|
942
|
+
currency: g('CURRENCY'),
|
|
943
|
+
transactionId: g('TRANSACTIONID'),
|
|
944
|
+
accountingMode: g('ACCOUNTINGMODE'),
|
|
945
|
+
authorMode: g('AUTHORMODE'),
|
|
946
|
+
result: g('RESULT'),
|
|
947
|
+
transactionType: g('TRANSACTIONTYPE') || undefined,
|
|
948
|
+
network: g('NETWORK') || undefined,
|
|
949
|
+
mac: g('MAC'),
|
|
950
|
+
// Optional fields
|
|
951
|
+
issuerCountry: g('ISSUERCOUNTRY') || undefined,
|
|
952
|
+
authCode: g('AUTHCODE') || undefined,
|
|
953
|
+
payerId: g('PAYERID') || undefined,
|
|
954
|
+
payer: g('PAYER') || undefined,
|
|
955
|
+
payerStatus: g('PAYERSTATUS') || undefined,
|
|
956
|
+
hashPan: g('HASHPAN') || undefined,
|
|
957
|
+
panAlias: g('PANALIAS') || undefined,
|
|
958
|
+
panAliasRev: g('PANALIASREV') || undefined,
|
|
959
|
+
panAliasExpDate: g('PANALIASEXPDATE') || undefined,
|
|
960
|
+
panAliasTail: g('PANALIASTAIL') || undefined,
|
|
961
|
+
maskedPan: g('MASKEDPAN') || undefined,
|
|
962
|
+
tRecurr: g('TRECURR') || undefined,
|
|
963
|
+
cRecurr: g('CRECURR') || undefined,
|
|
964
|
+
panTail: g('PANTAIL') || undefined,
|
|
965
|
+
panExpiryDate: g('PANEXPIRYDATE') || undefined,
|
|
966
|
+
accountHolder: g('ACCOUNTHOLDER') || undefined,
|
|
967
|
+
iban: g('IBAN') || undefined,
|
|
968
|
+
aliasStr: g('ALIASSTR') || undefined,
|
|
969
|
+
ahEmail: g('AHEMAIL') || undefined,
|
|
970
|
+
ahTaxId: g('AHTAXID') || undefined,
|
|
971
|
+
acquirerBin: g('ACQUIRERBIN') || undefined,
|
|
972
|
+
merchantId: g('MERCHANTID') || undefined,
|
|
973
|
+
cardType: g('CARDTYPE') || undefined,
|
|
974
|
+
amazonAuthId: g('AMAZONAUTHID') || undefined,
|
|
975
|
+
amazonCaptureId: g('AMAZONCAPTUREID') || undefined,
|
|
976
|
+
chInfo: g('CHINFO') || undefined,
|
|
977
|
+
panCode: g('PANCODE') || undefined,
|
|
978
|
+
installmentsNumber: g('INSTALLMENTSNUMBER') || undefined,
|
|
979
|
+
cardholderData: g('CARDHOLDERDATA') || undefined,
|
|
980
|
+
threeDsResult: g('THREEDSRESULT') || undefined,
|
|
981
|
+
subscriptionCode: g('SUBSCRIPTIONCODE') || undefined,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
985
|
+
// REDIRECT: Verify Outcome MAC (5.2.2)
|
|
986
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
987
|
+
/**
|
|
988
|
+
* Verify the MAC of a redirect outcome (URLMS/URLDONE) using the API-Result key.
|
|
989
|
+
* Per spec 5.2.2: MAC is NULL if result is negative (unless OPTION R).
|
|
990
|
+
* Returns true if MAC is valid, false otherwise.
|
|
991
|
+
*/
|
|
992
|
+
verifyOutcomeMAC(outcome) {
|
|
993
|
+
// If MAC is NULL and result is not 00, it's expected (unless OPTION R)
|
|
994
|
+
if (!outcome.mac || outcome.mac === 'NULL') {
|
|
995
|
+
return outcome.result !== '00';
|
|
996
|
+
}
|
|
997
|
+
// Build MAC string per spec 5.2.2 — field order is critical
|
|
998
|
+
// Required fields always present:
|
|
999
|
+
const macFields = [
|
|
1000
|
+
['ORDERID', outcome.orderId],
|
|
1001
|
+
['SHOPID', outcome.shopId],
|
|
1002
|
+
['AUTHNUMBER', outcome.authNumber],
|
|
1003
|
+
['AMOUNT', outcome.amount],
|
|
1004
|
+
['CURRENCY', outcome.currency],
|
|
1005
|
+
['TRANSACTIONID', outcome.transactionId],
|
|
1006
|
+
['ACCOUNTINGMODE', outcome.accountingMode],
|
|
1007
|
+
['AUTHORMODE', outcome.authorMode],
|
|
1008
|
+
['RESULT', outcome.result],
|
|
1009
|
+
['TRANSACTIONTYPE', outcome.transactionType],
|
|
1010
|
+
// Conditional fields — only included if present in the response
|
|
1011
|
+
['ISSUERCOUNTRY', outcome.issuerCountry],
|
|
1012
|
+
['AUTHCODE', outcome.authCode],
|
|
1013
|
+
['PAYERID', outcome.payerId],
|
|
1014
|
+
['PAYER', outcome.payer],
|
|
1015
|
+
['PAYERSTATUS', outcome.payerStatus],
|
|
1016
|
+
['HASHPAN', outcome.hashPan],
|
|
1017
|
+
['PANALIASREV', outcome.panAliasRev],
|
|
1018
|
+
['PANALIAS', outcome.panAlias],
|
|
1019
|
+
['PANALIASEXPDATE', outcome.panAliasExpDate],
|
|
1020
|
+
['PANALIASTAIL', outcome.panAliasTail],
|
|
1021
|
+
['MASKEDPAN', outcome.maskedPan],
|
|
1022
|
+
['TRECURR', outcome.tRecurr],
|
|
1023
|
+
['CRECURR', outcome.cRecurr],
|
|
1024
|
+
['PANTAIL', outcome.panTail],
|
|
1025
|
+
['PANEXPIRYDATE', outcome.panExpiryDate],
|
|
1026
|
+
['ACCOUNTHOLDER', outcome.accountHolder],
|
|
1027
|
+
['IBAN', outcome.iban],
|
|
1028
|
+
['ALIASSTR', outcome.aliasStr],
|
|
1029
|
+
['AHEMAIL', outcome.ahEmail],
|
|
1030
|
+
['AHTAXID', outcome.ahTaxId],
|
|
1031
|
+
['ACQUIRERBIN', outcome.acquirerBin],
|
|
1032
|
+
['MERCHANTID', outcome.merchantId],
|
|
1033
|
+
['CARDTYPE', outcome.cardType],
|
|
1034
|
+
['AMAZONAUTHID', outcome.amazonAuthId],
|
|
1035
|
+
['AMAZONCAPTUREID', outcome.amazonCaptureId],
|
|
1036
|
+
['CHINFO', outcome.chInfo],
|
|
1037
|
+
['PANCODE', outcome.panCode],
|
|
1038
|
+
['INSTALLMENTSNUMBER', outcome.installmentsNumber],
|
|
1039
|
+
['CARDHOLDERDATA', outcome.cardholderData],
|
|
1040
|
+
['THREEDSRESULT', outcome.threeDsResult],
|
|
1041
|
+
['SUBSCRIPTIONCODE', outcome.subscriptionCode],
|
|
1042
|
+
];
|
|
1043
|
+
const computed = (0, mac_1.generateMAC)(macFields, this.config.apiResultKey, this.config.hashAlgorithm);
|
|
1044
|
+
return computed.toLowerCase() === outcome.mac.toLowerCase();
|
|
1045
|
+
}
|
|
1046
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1047
|
+
// 3DS DATA ENCRYPTION (spec 5.4)
|
|
1048
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1049
|
+
/**
|
|
1050
|
+
* Encrypt 3DS 2.0 data for use in the 3DSDATA redirect field.
|
|
1051
|
+
* Uses AES/CBC/PKCS5Padding with first 16 bytes of API secret key, IV = 0.
|
|
1052
|
+
* Input: JSON object with 3DS fields. Output: Base64 string.
|
|
1053
|
+
*/
|
|
1054
|
+
encrypt3DSData(data) {
|
|
1055
|
+
const json = JSON.stringify(data);
|
|
1056
|
+
const keyBytes = crypto_js_1.default.enc.Utf8.parse(this.config.apiResultKey.substring(0, 16));
|
|
1057
|
+
const iv = crypto_js_1.default.lib.WordArray.create(new Uint8Array(16));
|
|
1058
|
+
const encrypted = crypto_js_1.default.AES.encrypt(crypto_js_1.default.enc.Utf8.parse(json), keyBytes, { iv, mode: crypto_js_1.default.mode.CBC, padding: crypto_js_1.default.pad.Pkcs7 });
|
|
1059
|
+
return encrypted.ciphertext.toString(crypto_js_1.default.enc.Base64);
|
|
1060
|
+
}
|
|
606
1061
|
}
|
|
607
1062
|
exports.VposClient = VposClient;
|
|
1063
|
+
// ─── Helper: Build HTML Form ──────────────────────────────────────────────────
|
|
1064
|
+
function buildHtmlForm(url, fields) {
|
|
1065
|
+
const inputs = Object.entries(fields)
|
|
1066
|
+
.map(([name, value]) => {
|
|
1067
|
+
const escaped = value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
1068
|
+
return ` <input type="hidden" name="${name}" value="${escaped}">`;
|
|
1069
|
+
})
|
|
1070
|
+
.join('\n');
|
|
1071
|
+
return [
|
|
1072
|
+
`<form id="vpos-redirect-form" method="POST" action="${url}">`,
|
|
1073
|
+
inputs,
|
|
1074
|
+
' <button type="submit">Proceed to payment</button>',
|
|
1075
|
+
'</form>',
|
|
1076
|
+
].join('\n');
|
|
1077
|
+
}
|
|
608
1078
|
//# sourceMappingURL=vpos-client.js.map
|