react-native-amwal-ecr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +20 -0
  3. package/README.md +278 -0
  4. package/RNAmwalEcr.podspec +27 -0
  5. package/android/build.gradle +53 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/com/amwalecr/AmwalEcrModule.kt +325 -0
  8. package/android/src/main/java/com/amwalecr/AmwalEcrPackage.kt +31 -0
  9. package/android/src/main/java/com/amwalecr/EcrMapping.kt +135 -0
  10. package/docs/compatibility.md +22 -0
  11. package/ios/AmwalEcr.h +4 -0
  12. package/ios/AmwalEcr.mm +121 -0
  13. package/ios/AmwalEcr.swift +424 -0
  14. package/ios/EcrMapping.swift +142 -0
  15. package/lib/module/EcrOperation.js +18 -0
  16. package/lib/module/EcrOperation.js.map +1 -0
  17. package/lib/module/EcrTerminal.js +211 -0
  18. package/lib/module/EcrTerminal.js.map +1 -0
  19. package/lib/module/NativeAmwalEcr.js +5 -0
  20. package/lib/module/NativeAmwalEcr.js.map +1 -0
  21. package/lib/module/index.js +6 -0
  22. package/lib/module/index.js.map +1 -0
  23. package/lib/module/native.js +16 -0
  24. package/lib/module/native.js.map +1 -0
  25. package/lib/module/native.native.js +4 -0
  26. package/lib/module/native.native.js.map +1 -0
  27. package/lib/module/normalize.js +87 -0
  28. package/lib/module/normalize.js.map +1 -0
  29. package/lib/module/package.json +1 -0
  30. package/lib/module/types.js +2 -0
  31. package/lib/module/types.js.map +1 -0
  32. package/lib/typescript/package.json +1 -0
  33. package/lib/typescript/src/EcrOperation.d.ts +10 -0
  34. package/lib/typescript/src/EcrOperation.d.ts.map +1 -0
  35. package/lib/typescript/src/EcrTerminal.d.ts +34 -0
  36. package/lib/typescript/src/EcrTerminal.d.ts.map +1 -0
  37. package/lib/typescript/src/NativeAmwalEcr.d.ts +14 -0
  38. package/lib/typescript/src/NativeAmwalEcr.d.ts.map +1 -0
  39. package/lib/typescript/src/index.d.ts +5 -0
  40. package/lib/typescript/src/index.d.ts.map +1 -0
  41. package/lib/typescript/src/native.d.ts +3 -0
  42. package/lib/typescript/src/native.d.ts.map +1 -0
  43. package/lib/typescript/src/native.native.d.ts +2 -0
  44. package/lib/typescript/src/native.native.d.ts.map +1 -0
  45. package/lib/typescript/src/normalize.d.ts +5 -0
  46. package/lib/typescript/src/normalize.d.ts.map +1 -0
  47. package/lib/typescript/src/types.d.ts +157 -0
  48. package/lib/typescript/src/types.d.ts.map +1 -0
  49. package/package.json +153 -0
  50. package/src/EcrOperation.ts +25 -0
  51. package/src/EcrTerminal.ts +344 -0
  52. package/src/NativeAmwalEcr.ts +28 -0
  53. package/src/index.tsx +34 -0
  54. package/src/native.native.ts +1 -0
  55. package/src/native.ts +18 -0
  56. package/src/normalize.ts +116 -0
  57. package/src/types.ts +189 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * How the till reaches the terminal.
3
+ *
4
+ * `wifi` and `ethernet` open a TCP socket to the terminal's local IP.
5
+ * `webService` drives the terminal through the Amwal payment host over REST —
6
+ * no local network involved; it needs `merchantId`, `terminalId` and
7
+ * `environment` in the config instead of a host. `usbCable` and `bluetooth`
8
+ * are not handled by this library and answer with a typed `unsupported`
9
+ * failure without sending anything.
10
+ */
11
+ export type EcrTransport = 'ethernet' | 'wifi' | 'bluetooth' | 'usbCable' | 'webService';
12
+ /**
13
+ * Hub deployment for Web Service ECR. URLs are resolved inside the native
14
+ * SDKs; integrators pass only this name.
15
+ */
16
+ export type EcrEnvironment = 'SIT' | 'UAT' | 'PROD';
17
+ export interface EcrConfig {
18
+ ecrId?: string;
19
+ currencyCode?: string;
20
+ minorUnitDigits?: number;
21
+ port?: number;
22
+ connectTimeoutMs?: number;
23
+ responseTimeoutMs?: number;
24
+ probeTimeoutMs?: number;
25
+ secureHashKey?: string;
26
+ autoInquireOnFailure?: boolean;
27
+ /** Backend merchant ID — required for the `webService` transport. */
28
+ merchantId?: string;
29
+ /** Backend terminal ID — required for the `webService` transport. */
30
+ terminalId?: string;
31
+ /** Hub deployment for the `webService` transport. Defaults to `'SIT'`. */
32
+ environment?: EcrEnvironment;
33
+ }
34
+ export interface EcrTerminalOptions {
35
+ /**
36
+ * The terminal's IP address. Required for `wifi` and `ethernet`; ignored by
37
+ * `webService`, which addresses the terminal through the payment host.
38
+ */
39
+ host?: string;
40
+ serialNumber?: string;
41
+ transport?: EcrTransport;
42
+ config?: EcrConfig;
43
+ }
44
+ export type EcrNextStep = 'NONE' | 'INQUIRE_BY_MERCHANT_REFERENCE';
45
+ export type EcrFailureKind = 'unreachable' | 'timeout' | 'malformed' | 'connectionLost' | 'unauthenticated' | 'cancelled' | 'unsupported';
46
+ export interface EcrFailure {
47
+ kind: EcrFailureKind;
48
+ message: string;
49
+ outcomeIsUnknown: boolean;
50
+ }
51
+ export interface EcrApproved {
52
+ outcome: 'approved';
53
+ merchantReferenceId: string;
54
+ amount: string;
55
+ responseCode: string;
56
+ rrn: string;
57
+ authCode: string;
58
+ maskedPan: string;
59
+ partialApproval: boolean;
60
+ requestedAmount: string;
61
+ raw: string;
62
+ outcomeIsUnknown: false;
63
+ nextStep: 'NONE';
64
+ }
65
+ export interface EcrDeclined {
66
+ outcome: 'declined';
67
+ merchantReferenceId: string;
68
+ responseCode: string;
69
+ reason: string;
70
+ nextStep: EcrNextStep;
71
+ raw: string;
72
+ outcomeIsUnknown: boolean;
73
+ }
74
+ export interface EcrFailed {
75
+ outcome: 'failed';
76
+ merchantReferenceId: string;
77
+ failure: EcrFailure;
78
+ recovered?: EcrInquiry;
79
+ settled: boolean;
80
+ outcomeIsUnknown: boolean;
81
+ nextStep: EcrNextStep;
82
+ }
83
+ export type EcrResult = EcrApproved | EcrDeclined | EcrFailed;
84
+ export interface EcrTransaction {
85
+ transactionId: string;
86
+ stan: string;
87
+ type: string;
88
+ status: string;
89
+ amount: string;
90
+ totalAmount: string;
91
+ currency: string;
92
+ transactionTime: string;
93
+ maskedPan: string;
94
+ cardHolderName: string;
95
+ rrn: string;
96
+ authCode: string;
97
+ batchId: string;
98
+ terminalId: string;
99
+ isRefunded: boolean;
100
+ canVoid: boolean;
101
+ canRefund: boolean;
102
+ partialApproval: boolean;
103
+ authorizedAmount: string;
104
+ }
105
+ export interface EcrInquiryFound {
106
+ outcome: 'found';
107
+ merchantReferenceId: string;
108
+ transaction: EcrTransaction;
109
+ raw: string;
110
+ }
111
+ export interface EcrInquiryNotFound {
112
+ outcome: 'notFound';
113
+ merchantReferenceId: string;
114
+ reason: string;
115
+ raw: string;
116
+ }
117
+ export interface EcrInquiryFailed {
118
+ outcome: 'failed';
119
+ merchantReferenceId: string;
120
+ failure: EcrFailure;
121
+ }
122
+ export type EcrInquiry = EcrInquiryFound | EcrInquiryNotFound | EcrInquiryFailed;
123
+ export interface EcrReceiptReady {
124
+ outcome: 'ready';
125
+ merchantReferenceId: string;
126
+ url: string;
127
+ raw: string;
128
+ }
129
+ export interface EcrReceiptUnavailable {
130
+ outcome: 'unavailable';
131
+ merchantReferenceId: string;
132
+ reason: string;
133
+ raw: string;
134
+ }
135
+ export interface EcrReceiptFailed {
136
+ outcome: 'failed';
137
+ merchantReferenceId: string;
138
+ failure: EcrFailure;
139
+ }
140
+ export type EcrReceipt = EcrReceiptReady | EcrReceiptUnavailable | EcrReceiptFailed;
141
+ export interface EcrReferenceOptions {
142
+ merchantReferenceId?: string;
143
+ operationId?: string;
144
+ }
145
+ export interface EcrOriginalTransactionOptions extends EcrReferenceOptions {
146
+ originalTerminalId?: string;
147
+ }
148
+ export interface EcrDatedTransactionOptions extends EcrOriginalTransactionOptions {
149
+ receiptNumber: string;
150
+ transactionDate: string;
151
+ }
152
+ export interface EcrRefundOptions extends EcrDatedTransactionOptions {
153
+ }
154
+ export interface EcrInquiryByReferenceOptions extends EcrOriginalTransactionOptions {
155
+ transactionDate?: string;
156
+ }
157
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,153 @@
1
+ {
2
+ "name": "react-native-amwal-ecr",
3
+ "version": "0.1.0",
4
+ "description": "React Native bridge for the Amwal ECR SDK on Android and iOS.",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "react-native-amwal-ecr-source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "docs",
22
+ "CHANGELOG.md",
23
+ "LICENSE",
24
+ "*.podspec",
25
+ "react-native.config.js",
26
+ "!ios/build",
27
+ "!android/build",
28
+ "!android/gradle",
29
+ "!android/gradlew",
30
+ "!android/gradlew.bat",
31
+ "!android/local.properties",
32
+ "!**/__tests__",
33
+ "!**/__fixtures__",
34
+ "!**/__mocks__",
35
+ "!**/.*"
36
+ ],
37
+ "scripts": {
38
+ "example": "yarn workspace react-native-amwal-ecr-example",
39
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
40
+ "prepare": "bob build",
41
+ "test": "jest",
42
+ "test:ci": "jest --ci --runInBand --coverage",
43
+ "typecheck": "tsc",
44
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
45
+ "verify": "yarn lint && yarn typecheck && yarn test:ci && yarn prepare && npm pack --dry-run"
46
+ },
47
+ "keywords": [
48
+ "react-native",
49
+ "ios",
50
+ "android",
51
+ "ecr",
52
+ "pos",
53
+ "payments",
54
+ "terminal"
55
+ ],
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/amwal-pay/amwal-ecr-rn.git"
59
+ },
60
+ "author": "Amwal Pay <support@amwal-pay.com> (https://amwal-pay.com)",
61
+ "license": "MIT",
62
+ "bugs": {
63
+ "url": "https://github.com/amwal-pay/amwal-ecr-rn/issues"
64
+ },
65
+ "homepage": "https://github.com/amwal-pay/amwal-ecr-rn#readme",
66
+ "publishConfig": {
67
+ "registry": "https://registry.npmjs.org/"
68
+ },
69
+ "devDependencies": {
70
+ "@eslint/compat": "^2.1.0",
71
+ "@eslint/eslintrc": "^3.3.5",
72
+ "@eslint/js": "^10.0.1",
73
+ "@react-native/babel-preset": "0.87.1",
74
+ "@react-native/eslint-config": "0.85.0",
75
+ "@react-native/jest-preset": "0.87.1",
76
+ "@types/jest": "^30.0.0",
77
+ "@types/react": "^19.2.0",
78
+ "del-cli": "^7.0.0",
79
+ "eslint": "^9.39.4",
80
+ "eslint-config-prettier": "^10.1.8",
81
+ "eslint-plugin-ft-flow": "^3.0.11",
82
+ "eslint-plugin-prettier": "^5.5.6",
83
+ "jest": "^30.2.0",
84
+ "prettier": "^3.8.3",
85
+ "react": "19.2.3",
86
+ "react-native": "0.87.1",
87
+ "react-native-builder-bob": "^0.43.0",
88
+ "turbo": "^2.9.16",
89
+ "typescript": "^6.0.3"
90
+ },
91
+ "peerDependencies": {
92
+ "react": "*",
93
+ "react-native": "*"
94
+ },
95
+ "workspaces": [
96
+ "example"
97
+ ],
98
+ "packageManager": "yarn@4.11.0",
99
+ "react-native-builder-bob": {
100
+ "source": "src",
101
+ "output": "lib",
102
+ "targets": [
103
+ [
104
+ "module",
105
+ {
106
+ "esm": true
107
+ }
108
+ ],
109
+ [
110
+ "typescript",
111
+ {
112
+ "project": "tsconfig.build.json"
113
+ }
114
+ ]
115
+ ]
116
+ },
117
+ "codegenConfig": {
118
+ "name": "AmwalEcrSpec",
119
+ "type": "modules",
120
+ "jsSrcsDir": "src",
121
+ "android": {
122
+ "javaPackageName": "com.amwalecr"
123
+ }
124
+ },
125
+ "jest": {
126
+ "preset": "@react-native/jest-preset",
127
+ "watchman": false,
128
+ "modulePathIgnorePatterns": [
129
+ "<rootDir>/example/node_modules",
130
+ "<rootDir>/lib/"
131
+ ],
132
+ "collectCoverageFrom": [
133
+ "src/**/*.{ts,tsx}",
134
+ "!src/NativeAmwalEcr.ts",
135
+ "!src/**/*.native.ts"
136
+ ]
137
+ },
138
+ "prettier": {
139
+ "quoteProps": "consistent",
140
+ "singleQuote": true,
141
+ "tabWidth": 2,
142
+ "trailingComma": "es5",
143
+ "useTabs": false
144
+ },
145
+ "create-react-native-library": {
146
+ "type": "turbo-module",
147
+ "languages": "kotlin-objc",
148
+ "tools": [
149
+ "eslint"
150
+ ],
151
+ "version": "0.63.0"
152
+ }
153
+ }
@@ -0,0 +1,25 @@
1
+ export class EcrOperation<T> {
2
+ readonly operationId: string;
3
+ readonly result: Promise<T>;
4
+ private readonly onCancel: (operationId: string) => Promise<boolean>;
5
+ private cancelRequested = false;
6
+
7
+ constructor(
8
+ operationId: string,
9
+ result: Promise<T>,
10
+ onCancel: (operationId: string) => Promise<boolean>
11
+ ) {
12
+ this.operationId = operationId;
13
+ this.result = result;
14
+ this.onCancel = onCancel;
15
+ }
16
+
17
+ get isCancelRequested(): boolean {
18
+ return this.cancelRequested;
19
+ }
20
+
21
+ cancel(): Promise<boolean> {
22
+ this.cancelRequested = true;
23
+ return this.onCancel(this.operationId);
24
+ }
25
+ }
@@ -0,0 +1,344 @@
1
+ import { EcrOperation } from './EcrOperation';
2
+ import { nativeEcr } from './native';
3
+ import {
4
+ normalizeInquiry,
5
+ normalizeReceipt,
6
+ normalizeResult,
7
+ } from './normalize';
8
+ import type {
9
+ EcrConfig,
10
+ EcrDatedTransactionOptions,
11
+ EcrEnvironment,
12
+ EcrInquiry,
13
+ EcrInquiryByReferenceOptions,
14
+ EcrOriginalTransactionOptions,
15
+ EcrReceipt,
16
+ EcrReferenceOptions,
17
+ EcrRefundOptions,
18
+ EcrResult,
19
+ EcrTerminalOptions,
20
+ EcrTransport,
21
+ } from './types';
22
+
23
+ const DEFAULT_CONFIG: Required<EcrConfig> = {
24
+ ecrId: 'ECR01',
25
+ currencyCode: '512',
26
+ minorUnitDigits: 3,
27
+ port: 9100,
28
+ connectTimeoutMs: 10_000,
29
+ responseTimeoutMs: 120_000,
30
+ probeTimeoutMs: 3_000,
31
+ secureHashKey: '',
32
+ autoInquireOnFailure: true,
33
+ merchantId: '',
34
+ terminalId: '',
35
+ environment: 'SIT',
36
+ };
37
+
38
+ const ENVIRONMENTS: readonly EcrEnvironment[] = ['SIT', 'UAT', 'PROD'];
39
+
40
+ let operationSequence = 0;
41
+
42
+ function newOperationId(): string {
43
+ operationSequence = (operationSequence + 1) % Number.MAX_SAFE_INTEGER;
44
+ return `ecr-${Date.now().toString(36)}-${operationSequence.toString(36)}-${Math.random()
45
+ .toString(36)
46
+ .slice(2, 10)}`;
47
+ }
48
+
49
+ function requireText(value: string, label: string): string {
50
+ const trimmed = value.trim();
51
+ if (!trimmed) {
52
+ throw new TypeError(`${label} is required.`);
53
+ }
54
+ return trimmed;
55
+ }
56
+
57
+ function requireAmount(amount: string): string {
58
+ const trimmed = amount.trim();
59
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) {
60
+ throw new TypeError(
61
+ `"${amount}" is not a plain decimal amount. Use a string such as "1.234".`
62
+ );
63
+ }
64
+ return trimmed;
65
+ }
66
+
67
+ function requireDate(value: string, required: boolean): string {
68
+ const trimmed = value.trim();
69
+ if (!trimmed && !required) return '';
70
+ if (!/^\d{8}$/.test(trimmed)) {
71
+ throw new TypeError(
72
+ 'transactionDate must use yyyyMMdd, for example 20260902.'
73
+ );
74
+ }
75
+ return trimmed;
76
+ }
77
+
78
+ function requireReference(value: string | undefined, label: string): string {
79
+ const trimmed = value?.trim() ?? '';
80
+ if (!trimmed) return '';
81
+ if (trimmed.length > 32 || /[\s&=]/.test(trimmed)) {
82
+ throw new TypeError(
83
+ `${label} must be at most 32 characters and cannot contain whitespace, "&", or "=".`
84
+ );
85
+ }
86
+ return trimmed;
87
+ }
88
+
89
+ function positiveInteger(value: number, label: string): number {
90
+ if (!Number.isInteger(value) || value <= 0) {
91
+ throw new TypeError(`${label} must be a positive whole number.`);
92
+ }
93
+ return value;
94
+ }
95
+
96
+ function normalizeConfig(config: EcrConfig = {}): Required<EcrConfig> {
97
+ const normalized = { ...DEFAULT_CONFIG, ...config };
98
+ if (normalized.minorUnitDigits < 0 || normalized.minorUnitDigits > 4) {
99
+ throw new TypeError('minorUnitDigits must be between 0 and 4.');
100
+ }
101
+ if (normalized.port < 1 || normalized.port > 65_535) {
102
+ throw new TypeError('port must be between 1 and 65535.');
103
+ }
104
+ positiveInteger(normalized.connectTimeoutMs, 'connectTimeoutMs');
105
+ positiveInteger(normalized.responseTimeoutMs, 'responseTimeoutMs');
106
+ positiveInteger(normalized.probeTimeoutMs, 'probeTimeoutMs');
107
+ if (
108
+ normalized.secureHashKey &&
109
+ (normalized.secureHashKey.length < 16 ||
110
+ normalized.secureHashKey.length % 2 !== 0 ||
111
+ !/^[0-9a-fA-F]+$/.test(normalized.secureHashKey))
112
+ ) {
113
+ throw new TypeError(
114
+ 'secureHashKey must be an even-length hexadecimal string of at least 16 characters, or empty.'
115
+ );
116
+ }
117
+ const environment = String(normalized.environment).toUpperCase();
118
+ if (!ENVIRONMENTS.includes(environment as EcrEnvironment)) {
119
+ throw new TypeError(
120
+ `environment must be one of ${ENVIRONMENTS.join(', ')}.`
121
+ );
122
+ }
123
+ normalized.environment = environment as EcrEnvironment;
124
+ return normalized;
125
+ }
126
+
127
+ type NativeRequest = Record<string, unknown>;
128
+
129
+ /**
130
+ * One Amwal POS terminal reachable from the mobile device's local network.
131
+ *
132
+ * Amounts are decimal strings, never JavaScript numbers. A failed exchange is
133
+ * not a decline: when `outcomeIsUnknown` is true, inquire by the same merchant
134
+ * reference before considering another money-moving request.
135
+ */
136
+ export class EcrTerminal {
137
+ readonly host: string;
138
+ readonly serialNumber: string;
139
+ readonly transport: EcrTransport;
140
+ readonly config: Required<EcrConfig>;
141
+
142
+ constructor(options: EcrTerminalOptions) {
143
+ this.serialNumber = options.serialNumber?.trim() ?? '';
144
+ this.transport = options.transport ?? 'wifi';
145
+ this.config = normalizeConfig(options.config);
146
+ if (this.transport === 'webService') {
147
+ // The payment host addresses the terminal; there is no IP to dial.
148
+ this.host = options.host?.trim() ?? '';
149
+ requireText(this.config.merchantId, 'config.merchantId');
150
+ requireText(this.config.terminalId, 'config.terminalId');
151
+ } else {
152
+ this.host = requireText(options.host ?? '', 'host');
153
+ }
154
+ }
155
+
156
+ isReachable(): Promise<boolean> {
157
+ if (!this.isIpTransport) return Promise.resolve(false);
158
+ return nativeEcr.isReachable(this.request(newOperationId()));
159
+ }
160
+
161
+ sale(amount: string, options: EcrReferenceOptions = {}): Promise<EcrResult> {
162
+ return this.startSale(amount, options).result;
163
+ }
164
+
165
+ startSale(
166
+ amount: string,
167
+ options: EcrReferenceOptions = {}
168
+ ): EcrOperation<EcrResult> {
169
+ return this.startResult('sale', {
170
+ amount: requireAmount(amount),
171
+ merchantReferenceId: requireReference(
172
+ options.merchantReferenceId,
173
+ 'merchantReferenceId'
174
+ ),
175
+ operationId: options.operationId,
176
+ });
177
+ }
178
+
179
+ voidTransaction(
180
+ receiptNumber: string,
181
+ options: EcrOriginalTransactionOptions = {}
182
+ ): Promise<EcrResult> {
183
+ return this.startVoid(receiptNumber, options).result;
184
+ }
185
+
186
+ startVoid(
187
+ receiptNumber: string,
188
+ options: EcrOriginalTransactionOptions = {}
189
+ ): EcrOperation<EcrResult> {
190
+ return this.startResult('voidTransaction', {
191
+ receiptNumber: requireText(receiptNumber, 'receiptNumber'),
192
+ originalTerminalId: options.originalTerminalId?.trim() ?? '',
193
+ merchantReferenceId: requireReference(
194
+ options.merchantReferenceId,
195
+ 'merchantReferenceId'
196
+ ),
197
+ operationId: options.operationId,
198
+ });
199
+ }
200
+
201
+ refund(amount: string, options: EcrRefundOptions): Promise<EcrResult> {
202
+ return this.startRefund(amount, options).result;
203
+ }
204
+
205
+ startRefund(
206
+ amount: string,
207
+ options: EcrRefundOptions
208
+ ): EcrOperation<EcrResult> {
209
+ return this.startResult('refund', {
210
+ amount: requireAmount(amount),
211
+ receiptNumber: requireText(options.receiptNumber, 'receiptNumber'),
212
+ transactionDate: requireDate(options.transactionDate, true),
213
+ originalTerminalId: options.originalTerminalId?.trim() ?? '',
214
+ merchantReferenceId: requireReference(
215
+ options.merchantReferenceId,
216
+ 'merchantReferenceId'
217
+ ),
218
+ operationId: options.operationId,
219
+ });
220
+ }
221
+
222
+ inquire(options: EcrDatedTransactionOptions): Promise<EcrInquiry> {
223
+ return this.startInquire(options).result;
224
+ }
225
+
226
+ startInquire(options: EcrDatedTransactionOptions): EcrOperation<EcrInquiry> {
227
+ return this.startOperation(
228
+ 'inquire',
229
+ {
230
+ receiptNumber: requireText(options.receiptNumber, 'receiptNumber'),
231
+ transactionDate: requireDate(options.transactionDate, true),
232
+ originalTerminalId: options.originalTerminalId?.trim() ?? '',
233
+ merchantReferenceId: requireReference(
234
+ options.merchantReferenceId,
235
+ 'merchantReferenceId'
236
+ ),
237
+ operationId: options.operationId,
238
+ },
239
+ normalizeInquiry
240
+ );
241
+ }
242
+
243
+ inquireByReference(
244
+ originalMerchantReference: string,
245
+ options: EcrInquiryByReferenceOptions = {}
246
+ ): Promise<EcrInquiry> {
247
+ return this.startInquireByReference(originalMerchantReference, options)
248
+ .result;
249
+ }
250
+
251
+ startInquireByReference(
252
+ originalMerchantReference: string,
253
+ options: EcrInquiryByReferenceOptions = {}
254
+ ): EcrOperation<EcrInquiry> {
255
+ const originalReference = requireReference(
256
+ originalMerchantReference,
257
+ 'originalMerchantReference'
258
+ );
259
+ requireText(originalReference, 'originalMerchantReference');
260
+ return this.startOperation(
261
+ 'inquireByReference',
262
+ {
263
+ originalMerchantReference: originalReference,
264
+ transactionDate: requireDate(options.transactionDate ?? '', false),
265
+ originalTerminalId: options.originalTerminalId?.trim() ?? '',
266
+ merchantReferenceId: requireReference(
267
+ options.merchantReferenceId,
268
+ 'merchantReferenceId'
269
+ ),
270
+ operationId: options.operationId,
271
+ },
272
+ normalizeInquiry
273
+ );
274
+ }
275
+
276
+ receipt(options: EcrDatedTransactionOptions): Promise<EcrReceipt> {
277
+ return this.startReceipt(options).result;
278
+ }
279
+
280
+ startReceipt(options: EcrDatedTransactionOptions): EcrOperation<EcrReceipt> {
281
+ return this.startOperation(
282
+ 'receipt',
283
+ {
284
+ receiptNumber: requireText(options.receiptNumber, 'receiptNumber'),
285
+ transactionDate: requireDate(options.transactionDate, true),
286
+ originalTerminalId: options.originalTerminalId?.trim() ?? '',
287
+ merchantReferenceId: requireReference(
288
+ options.merchantReferenceId,
289
+ 'merchantReferenceId'
290
+ ),
291
+ operationId: options.operationId,
292
+ },
293
+ normalizeReceipt
294
+ );
295
+ }
296
+
297
+ private get isIpTransport(): boolean {
298
+ return this.transport === 'ethernet' || this.transport === 'wifi';
299
+ }
300
+
301
+ private request(
302
+ operationId: string,
303
+ values: NativeRequest = {}
304
+ ): NativeRequest {
305
+ return {
306
+ operationId: requireText(operationId, 'operationId'),
307
+ host: this.host,
308
+ serialNumber: this.serialNumber,
309
+ transport: this.transport,
310
+ config: this.config,
311
+ ...values,
312
+ };
313
+ }
314
+
315
+ private startResult(
316
+ method: 'sale' | 'voidTransaction' | 'refund',
317
+ values: NativeRequest
318
+ ): EcrOperation<EcrResult> {
319
+ return this.startOperation(method, values, normalizeResult);
320
+ }
321
+
322
+ private startOperation<T>(
323
+ method:
324
+ | 'sale'
325
+ | 'voidTransaction'
326
+ | 'refund'
327
+ | 'inquire'
328
+ | 'inquireByReference'
329
+ | 'receipt',
330
+ values: NativeRequest,
331
+ normalize: (value: unknown) => T
332
+ ): EcrOperation<T> {
333
+ const suppliedId = values.operationId;
334
+ const operationId =
335
+ typeof suppliedId === 'string' && suppliedId.trim()
336
+ ? suppliedId.trim()
337
+ : newOperationId();
338
+ const requestValues = { ...values };
339
+ delete requestValues.operationId;
340
+ const request = this.request(operationId, requestValues);
341
+ const result = nativeEcr[method](request).then(normalize);
342
+ return new EcrOperation(operationId, result, (id) => nativeEcr.cancel(id));
343
+ }
344
+ }