@munchi_oy/payments 1.2.5
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 +28 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/dist/src/MunchiPaymentSDK.d.ts +42 -0
- package/dist/src/MunchiPaymentSDK.d.ts.map +1 -0
- package/dist/src/error.d.ts +18 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/strategies/IPaymentStrategy.d.ts +12 -0
- package/dist/src/strategies/IPaymentStrategy.d.ts.map +1 -0
- package/dist/src/strategies/MockStrategy.d.ts +13 -0
- package/dist/src/strategies/MockStrategy.d.ts.map +1 -0
- package/dist/src/strategies/NetsStrategy.d.ts +24 -0
- package/dist/src/strategies/NetsStrategy.d.ts.map +1 -0
- package/dist/src/strategies/VivaStrategy.d.ts +24 -0
- package/dist/src/strategies/VivaStrategy.d.ts.map +1 -0
- package/dist/src/test-link.d.ts +2 -0
- package/dist/src/test-link.d.ts.map +1 -0
- package/dist/src/types/device.d.ts +15 -0
- package/dist/src/types/device.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +4 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/payment.d.ts +109 -0
- package/dist/src/types/payment.d.ts.map +1 -0
- package/dist/src/types/sdk.d.ts +48 -0
- package/dist/src/types/sdk.d.ts.map +1 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.d.ts.map +1 -0
- package/index.ts +3 -0
- package/package.json +51 -0
- package/src/MunchiPaymentSDK.ts +532 -0
- package/src/error.ts +26 -0
- package/src/strategies/IPaymentStrategy.ts +22 -0
- package/src/strategies/MockStrategy.ts +55 -0
- package/src/strategies/NetsStrategy.ts +336 -0
- package/src/strategies/VivaStrategy.ts +319 -0
- package/src/test-link.ts +0 -0
- package/src/types/device.ts +15 -0
- package/src/types/index.ts +3 -0
- package/src/types/payment.ts +123 -0
- package/src/types/sdk.ts +42 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CreateNetsTerminalPaymentDto,
|
|
3
|
+
type NetsCancelPayloadDto,
|
|
4
|
+
type NetsCancelTransactionDto,
|
|
5
|
+
PaymentApi,
|
|
6
|
+
PaymentEventType,
|
|
7
|
+
PaymentProviderEnum,
|
|
8
|
+
type PaymentStatusDto,
|
|
9
|
+
SimplePaymentStatus,
|
|
10
|
+
type TransactionDto,
|
|
11
|
+
TransactionType,
|
|
12
|
+
} from "@munchi_oy/core";
|
|
13
|
+
import type { AxiosInstance } from "axios";
|
|
14
|
+
|
|
15
|
+
import { PaymentErrorCode, PaymentSDKError } from "../error";
|
|
16
|
+
import {
|
|
17
|
+
type IMessagingAdapter,
|
|
18
|
+
PaymentInteractionState,
|
|
19
|
+
type PaymentRequest,
|
|
20
|
+
type PaymentResult,
|
|
21
|
+
type PaymentTerminalConfig,
|
|
22
|
+
type RefundRequest,
|
|
23
|
+
SdkPaymentStatus,
|
|
24
|
+
} from "../types/payment";
|
|
25
|
+
import type { IPaymentStrategy } from "./IPaymentStrategy";
|
|
26
|
+
|
|
27
|
+
export class NetsStrategy implements IPaymentStrategy {
|
|
28
|
+
private api: PaymentApi;
|
|
29
|
+
private abortController: AbortController | null = null;
|
|
30
|
+
private currentRequestId: string | null = null;
|
|
31
|
+
private paymentProvider = PaymentProviderEnum.Nets;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
axios: AxiosInstance,
|
|
35
|
+
private messaging: IMessagingAdapter,
|
|
36
|
+
private config: PaymentTerminalConfig,
|
|
37
|
+
) {
|
|
38
|
+
this.api = new PaymentApi(undefined, "", axios);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async processPayment(
|
|
42
|
+
request: PaymentRequest,
|
|
43
|
+
onStateChange: (
|
|
44
|
+
state: PaymentInteractionState,
|
|
45
|
+
detail?: { sessionId?: string },
|
|
46
|
+
) => void,
|
|
47
|
+
): Promise<PaymentResult> {
|
|
48
|
+
this.abortController = new AbortController();
|
|
49
|
+
onStateChange(PaymentInteractionState.CONNECTING);
|
|
50
|
+
|
|
51
|
+
const purchasePayload: CreateNetsTerminalPaymentDto = {
|
|
52
|
+
amount: request.amountCents,
|
|
53
|
+
businessId: Number(this.config.storeId),
|
|
54
|
+
referenceId: request.orderRef,
|
|
55
|
+
currency: request.currency,
|
|
56
|
+
displayId: request.displayId,
|
|
57
|
+
options: {
|
|
58
|
+
allowPinBypass: true,
|
|
59
|
+
transactionType: TransactionType.Purchase,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const { data } =
|
|
65
|
+
await this.api.initiateNetsTerminalTransaction(purchasePayload);
|
|
66
|
+
|
|
67
|
+
const requestId = data.connectCloudRequestId;
|
|
68
|
+
|
|
69
|
+
if (!requestId) {
|
|
70
|
+
throw new Error("connectCloudRequestId is missing from response.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.currentRequestId = requestId;
|
|
74
|
+
|
|
75
|
+
if (this.abortController.signal.aborted) {
|
|
76
|
+
throw new Error("Aborted");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onStateChange(PaymentInteractionState.REQUIRES_INPUT, {
|
|
80
|
+
sessionId: requestId,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = await this.waitForPaymentCompletion(
|
|
84
|
+
requestId,
|
|
85
|
+
request.orderRef,
|
|
86
|
+
this.abortController.signal,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
this.currentRequestId = null;
|
|
90
|
+
return result;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.currentRequestId = null;
|
|
93
|
+
if (err instanceof PaymentSDKError) throw err;
|
|
94
|
+
|
|
95
|
+
throw new PaymentSDKError(
|
|
96
|
+
PaymentErrorCode.TERMINAL_BUSY,
|
|
97
|
+
"Failed to create Nets Intent",
|
|
98
|
+
err,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async waitForPaymentCompletion(
|
|
104
|
+
requestId: string,
|
|
105
|
+
orderRef: string,
|
|
106
|
+
signal: AbortSignal,
|
|
107
|
+
): Promise<PaymentResult> {
|
|
108
|
+
const channelName = `nets.requests.${requestId}`;
|
|
109
|
+
const eventName = PaymentEventType.StatusChanged;
|
|
110
|
+
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
let isResolved = false;
|
|
113
|
+
|
|
114
|
+
const cleanup = () => {
|
|
115
|
+
isResolved = true;
|
|
116
|
+
if (typeof unsubscribe === "function") {
|
|
117
|
+
unsubscribe();
|
|
118
|
+
}
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const onAbort = () => {
|
|
123
|
+
cleanup();
|
|
124
|
+
reject(
|
|
125
|
+
new PaymentSDKError(
|
|
126
|
+
PaymentErrorCode.CANCELLED,
|
|
127
|
+
"Transaction cancelled",
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
signal.addEventListener("abort", onAbort);
|
|
133
|
+
|
|
134
|
+
const unsubscribe = this.messaging.subscribe<PaymentStatusDto>(
|
|
135
|
+
channelName,
|
|
136
|
+
eventName,
|
|
137
|
+
(data: PaymentStatusDto) => {
|
|
138
|
+
if (!isResolved) {
|
|
139
|
+
cleanup();
|
|
140
|
+
signal.removeEventListener("abort", onAbort);
|
|
141
|
+
resolve(this.handleSuccess(data));
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const timer = setTimeout(async () => {
|
|
147
|
+
if (isResolved || signal.aborted) return;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const finalResult = await this.pollOrderStatus(
|
|
151
|
+
requestId,
|
|
152
|
+
orderRef,
|
|
153
|
+
this.config.storeId,
|
|
154
|
+
signal,
|
|
155
|
+
);
|
|
156
|
+
resolve(this.handleSuccess(finalResult));
|
|
157
|
+
} catch (pollError) {
|
|
158
|
+
reject(
|
|
159
|
+
new PaymentSDKError(
|
|
160
|
+
PaymentErrorCode.TIMEOUT,
|
|
161
|
+
"Payment timed out and polling failed",
|
|
162
|
+
pollError,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
} finally {
|
|
166
|
+
signal.removeEventListener("abort", onAbort);
|
|
167
|
+
cleanup();
|
|
168
|
+
}
|
|
169
|
+
}, 10000);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async pollOrderStatus(
|
|
174
|
+
requestId: string,
|
|
175
|
+
orderRef: string,
|
|
176
|
+
businessId: string,
|
|
177
|
+
signal: AbortSignal,
|
|
178
|
+
): Promise<PaymentStatusDto> {
|
|
179
|
+
const POLLING_DURATION_MS = 120000;
|
|
180
|
+
const INTERVAL_MS = 2000;
|
|
181
|
+
const startTime = Date.now();
|
|
182
|
+
const deadline = startTime + POLLING_DURATION_MS;
|
|
183
|
+
|
|
184
|
+
while (Date.now() < deadline) {
|
|
185
|
+
if (signal.aborted) {
|
|
186
|
+
throw new Error("Aborted");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const { data } = await this.api.getPaymentStatus({
|
|
191
|
+
businessId: Number(businessId),
|
|
192
|
+
orderId: orderRef,
|
|
193
|
+
provider: this.paymentProvider,
|
|
194
|
+
referenceId: requestId,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (data.status !== SimplePaymentStatus.Pending) return data;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Payment verification failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, INTERVAL_MS));
|
|
204
|
+
}
|
|
205
|
+
throw new Error("Payment verification timed out.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async cancelTransaction(
|
|
209
|
+
_onStateChange: (state: PaymentInteractionState) => void,
|
|
210
|
+
): Promise<boolean> {
|
|
211
|
+
if (!this.currentRequestId) return false;
|
|
212
|
+
|
|
213
|
+
const payload: NetsCancelTransactionDto = {
|
|
214
|
+
requestId: this.currentRequestId,
|
|
215
|
+
businessId: Number(this.config.storeId),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await this.api.cancelNetsTerminalTransaction(payload);
|
|
220
|
+
this.abortController?.abort();
|
|
221
|
+
this.currentRequestId = null;
|
|
222
|
+
return true;
|
|
223
|
+
} catch (_e) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async refundTransaction(
|
|
229
|
+
request: RefundRequest,
|
|
230
|
+
onStateChange: (
|
|
231
|
+
state: PaymentInteractionState,
|
|
232
|
+
detail?: { sessionId?: string },
|
|
233
|
+
) => void,
|
|
234
|
+
): Promise<PaymentResult> {
|
|
235
|
+
try {
|
|
236
|
+
this.abortController = new AbortController();
|
|
237
|
+
|
|
238
|
+
const payload: CreateNetsTerminalPaymentDto = {
|
|
239
|
+
amount: request.amountCents,
|
|
240
|
+
businessId: Number(this.config.storeId),
|
|
241
|
+
currency: request.currency,
|
|
242
|
+
displayId: this.config.kioskId,
|
|
243
|
+
referenceId: request.orderRef,
|
|
244
|
+
options: {
|
|
245
|
+
allowPinBypass: true,
|
|
246
|
+
transactionType: TransactionType.ReturnOfGoods,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const { data } = await this.api.initiateNetsTerminalTransaction(payload);
|
|
251
|
+
|
|
252
|
+
const requestId = data.connectCloudRequestId;
|
|
253
|
+
|
|
254
|
+
if (!requestId) {
|
|
255
|
+
throw new Error("connectCloudRequestId is missing from response.");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.currentRequestId = requestId;
|
|
259
|
+
|
|
260
|
+
if (this.abortController.signal.aborted) {
|
|
261
|
+
throw new Error("Aborted");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
onStateChange(PaymentInteractionState.REQUIRES_INPUT, {
|
|
265
|
+
sessionId: requestId,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = await this.waitForPaymentCompletion(
|
|
269
|
+
requestId,
|
|
270
|
+
request.orderRef,
|
|
271
|
+
this.abortController.signal,
|
|
272
|
+
);
|
|
273
|
+
this.currentRequestId = null;
|
|
274
|
+
|
|
275
|
+
return result;
|
|
276
|
+
} catch (err) {
|
|
277
|
+
this.currentRequestId = null;
|
|
278
|
+
|
|
279
|
+
if (err instanceof PaymentSDKError) throw err;
|
|
280
|
+
|
|
281
|
+
if (err instanceof PaymentSDKError) throw err;
|
|
282
|
+
|
|
283
|
+
throw new PaymentSDKError(
|
|
284
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
285
|
+
"Failed to refund Nets transaction",
|
|
286
|
+
err,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async verifyFinalStatus(
|
|
292
|
+
request: PaymentRequest,
|
|
293
|
+
sessionId: string,
|
|
294
|
+
): Promise<PaymentResult> {
|
|
295
|
+
try {
|
|
296
|
+
const { data } = await this.api.getPaymentStatus({
|
|
297
|
+
businessId: Number(this.config.storeId),
|
|
298
|
+
orderId: request.orderRef,
|
|
299
|
+
provider: this.paymentProvider,
|
|
300
|
+
referenceId: sessionId,
|
|
301
|
+
});
|
|
302
|
+
return this.handleSuccess(data);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
throw new PaymentSDKError(
|
|
305
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
306
|
+
"Failed to verify final Nets status",
|
|
307
|
+
error,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private handleSuccess(data: PaymentStatusDto): PaymentResult {
|
|
313
|
+
const isSuccess = data.status === SimplePaymentStatus.Success;
|
|
314
|
+
|
|
315
|
+
const result: PaymentResult = {
|
|
316
|
+
success: isSuccess,
|
|
317
|
+
status: isSuccess ? SdkPaymentStatus.SUCCESS : SdkPaymentStatus.FAILED,
|
|
318
|
+
orderId: data.orderId,
|
|
319
|
+
transaction: data.transaction as unknown as TransactionDto,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
if (data.transactionId) {
|
|
323
|
+
result.transactionId = data.transactionId;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (data.error?.code) {
|
|
327
|
+
result.errorCode = data.error.code;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (data.error?.message) {
|
|
331
|
+
result.errorMessage = data.error.message;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CreateRefundDto,
|
|
3
|
+
type CreateTerminalPaymentDto,
|
|
4
|
+
PaymentApi,
|
|
5
|
+
PaymentProviderEnum,
|
|
6
|
+
type PaymentStatusDto,
|
|
7
|
+
SimplePaymentStatus,
|
|
8
|
+
type TransactionDto,
|
|
9
|
+
} from "@munchi_oy/core";
|
|
10
|
+
import type { AxiosInstance } from "axios";
|
|
11
|
+
|
|
12
|
+
import { PaymentErrorCode, PaymentSDKError } from "../error";
|
|
13
|
+
import {
|
|
14
|
+
type IMessagingAdapter,
|
|
15
|
+
PaymentInteractionState,
|
|
16
|
+
type PaymentRequest,
|
|
17
|
+
type PaymentResult,
|
|
18
|
+
type PaymentTerminalConfig,
|
|
19
|
+
type RefundRequest,
|
|
20
|
+
SdkPaymentStatus,
|
|
21
|
+
} from "../types/payment";
|
|
22
|
+
import type { IPaymentStrategy } from "./IPaymentStrategy";
|
|
23
|
+
|
|
24
|
+
export class VivaStrategy implements IPaymentStrategy {
|
|
25
|
+
private api: PaymentApi;
|
|
26
|
+
private abortController: AbortController | null = null;
|
|
27
|
+
private currentSessionId: string | null = null;
|
|
28
|
+
private paymentProvider = PaymentProviderEnum.Viva;
|
|
29
|
+
constructor(
|
|
30
|
+
axios: AxiosInstance,
|
|
31
|
+
private messaging: IMessagingAdapter,
|
|
32
|
+
private config: PaymentTerminalConfig,
|
|
33
|
+
) {
|
|
34
|
+
this.api = new PaymentApi(undefined, "", axios);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async processPayment(
|
|
40
|
+
request: PaymentRequest,
|
|
41
|
+
onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
|
|
42
|
+
): Promise<PaymentResult> {
|
|
43
|
+
this.abortController = new AbortController();
|
|
44
|
+
onStateChange(PaymentInteractionState.CONNECTING);
|
|
45
|
+
|
|
46
|
+
const payload: CreateTerminalPaymentDto = {
|
|
47
|
+
amount: request.amountCents,
|
|
48
|
+
referenceId: request.orderRef,
|
|
49
|
+
businessId: parseInt(this.config.storeId),
|
|
50
|
+
currency: request.currency,
|
|
51
|
+
displayId: request.displayId,
|
|
52
|
+
showReceipt: true,
|
|
53
|
+
showTransactionResult: true,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const { data } = await this.api.initiateTerminalTransaction(payload);
|
|
58
|
+
this.currentSessionId = data.sessionId;
|
|
59
|
+
|
|
60
|
+
if (this.abortController.signal.aborted) {
|
|
61
|
+
throw new Error("Aborted");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onStateChange(PaymentInteractionState.REQUIRES_INPUT, { sessionId: data.sessionId });
|
|
65
|
+
|
|
66
|
+
const result = await this.waitForPaymentCompletion(
|
|
67
|
+
data.sessionId,
|
|
68
|
+
request.orderRef,
|
|
69
|
+
this.abortController.signal,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
this.currentSessionId = null;
|
|
73
|
+
return result;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
this.currentSessionId = null;
|
|
76
|
+
if (err instanceof PaymentSDKError) throw err;
|
|
77
|
+
throw new PaymentSDKError(
|
|
78
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
79
|
+
"Failed to create Viva Intent",
|
|
80
|
+
err,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async waitForPaymentCompletion(
|
|
86
|
+
sessionId: string,
|
|
87
|
+
orderRef: string,
|
|
88
|
+
signal: AbortSignal,
|
|
89
|
+
): Promise<PaymentResult> {
|
|
90
|
+
const channel = this.config.channel.toLowerCase()
|
|
91
|
+
const channelName = `viva.${channel}.requests.${sessionId}`;
|
|
92
|
+
const eventName = "payment:status-changed";
|
|
93
|
+
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
let isResolved = false;
|
|
96
|
+
|
|
97
|
+
const cleanup = () => {
|
|
98
|
+
isResolved = true;
|
|
99
|
+
if (typeof unsubscribe === "function") {
|
|
100
|
+
unsubscribe();
|
|
101
|
+
}
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const onAbort = () => {
|
|
106
|
+
cleanup();
|
|
107
|
+
reject(new Error("Aborted"));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
signal.addEventListener("abort", onAbort);
|
|
111
|
+
|
|
112
|
+
const unsubscribe = this.messaging.subscribe<PaymentStatusDto>(
|
|
113
|
+
channelName,
|
|
114
|
+
eventName,
|
|
115
|
+
(data: PaymentStatusDto) => {
|
|
116
|
+
if (!isResolved) {
|
|
117
|
+
cleanup();
|
|
118
|
+
signal.removeEventListener("abort", onAbort);
|
|
119
|
+
resolve(this.handleSuccess(data));
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const timer = setTimeout(async () => {
|
|
125
|
+
if (isResolved || signal.aborted) return;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const finalResult = await this.pollOrderStatus(
|
|
129
|
+
sessionId,
|
|
130
|
+
orderRef,
|
|
131
|
+
this.config.storeId,
|
|
132
|
+
signal,
|
|
133
|
+
);
|
|
134
|
+
resolve(this.handleSuccess(finalResult));
|
|
135
|
+
} catch (pollError) {
|
|
136
|
+
// SDK handles the FAILED state when we reject
|
|
137
|
+
reject(
|
|
138
|
+
new PaymentSDKError(
|
|
139
|
+
PaymentErrorCode.TIMEOUT,
|
|
140
|
+
"Payment timed out and polling failed",
|
|
141
|
+
pollError,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
} finally {
|
|
145
|
+
signal.removeEventListener("abort", onAbort);
|
|
146
|
+
cleanup();
|
|
147
|
+
}
|
|
148
|
+
}, 10000);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async pollOrderStatus(
|
|
153
|
+
sessionId: string,
|
|
154
|
+
orderRef: string,
|
|
155
|
+
businessId: string,
|
|
156
|
+
signal: AbortSignal,
|
|
157
|
+
): Promise<PaymentStatusDto> {
|
|
158
|
+
const POLLING_DURATION_MS = 120000;
|
|
159
|
+
const INTERVAL_MS = 2000;
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
const deadline = startTime + POLLING_DURATION_MS;
|
|
162
|
+
|
|
163
|
+
while (Date.now() < deadline) {
|
|
164
|
+
if (signal.aborted) {
|
|
165
|
+
throw new Error("Aborted");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const { data } = await this.api.getPaymentStatus(
|
|
170
|
+
{
|
|
171
|
+
businessId: Number(businessId),
|
|
172
|
+
orderId: orderRef,
|
|
173
|
+
provider: this.paymentProvider,
|
|
174
|
+
referenceId: sessionId,
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
if (data.status !== SimplePaymentStatus.Pending) return data;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new Error(`Payment verification failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
}
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, INTERVAL_MS));
|
|
182
|
+
}
|
|
183
|
+
throw new Error("Payment verification timed out.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private handleSuccess(
|
|
187
|
+
data: PaymentStatusDto,
|
|
188
|
+
): PaymentResult {
|
|
189
|
+
const isSuccess = data.status === SimplePaymentStatus.Success;
|
|
190
|
+
|
|
191
|
+
const result: PaymentResult = {
|
|
192
|
+
success: isSuccess,
|
|
193
|
+
status: isSuccess ? SdkPaymentStatus.SUCCESS : SdkPaymentStatus.FAILED,
|
|
194
|
+
orderId: data.orderId,
|
|
195
|
+
errorCode: data.error?.code ?? "",
|
|
196
|
+
errorMessage: data.error?.message ?? "",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (data.transactionId) {
|
|
200
|
+
result.transactionId = data.transactionId;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (data.error?.referenceError) {
|
|
204
|
+
result.errorReference = data.error.referenceError;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (data.transaction) {
|
|
208
|
+
result.transaction = data.transaction as unknown as TransactionDto;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async cancelTransaction(
|
|
215
|
+
_onStateChange: (state: PaymentInteractionState) => void,
|
|
216
|
+
): Promise<boolean> {
|
|
217
|
+
if (!this.currentSessionId) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const sessionIdToCancel = this.currentSessionId;
|
|
223
|
+
this.currentSessionId = null;
|
|
224
|
+
await this.api.cancelVivaTransactionV2({
|
|
225
|
+
cashRegisterId: this.config.storeId,
|
|
226
|
+
sessionId: sessionIdToCancel,
|
|
227
|
+
});
|
|
228
|
+
this.abortController?.abort();
|
|
229
|
+
return true;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
// If we failed to cancel (e.g. 409 Conflict), we should NOT abort the controller
|
|
232
|
+
// because the transaction might still be valid/processing.
|
|
233
|
+
this.currentSessionId = null;
|
|
234
|
+
throw new PaymentSDKError(
|
|
235
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
236
|
+
"Failed to cancel Viva transaction",
|
|
237
|
+
error,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async verifyFinalStatus(request: PaymentRequest, sessionId: string): Promise<PaymentResult> {
|
|
243
|
+
try {
|
|
244
|
+
const { data } = await this.api.getPaymentStatus(
|
|
245
|
+
{
|
|
246
|
+
businessId: Number(this.config.storeId),
|
|
247
|
+
orderId: request.orderRef,
|
|
248
|
+
provider: this.paymentProvider,
|
|
249
|
+
referenceId: sessionId,
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const isSuccess = data.status === SimplePaymentStatus.Success;
|
|
254
|
+
|
|
255
|
+
const result: PaymentResult = {
|
|
256
|
+
success: isSuccess,
|
|
257
|
+
status: isSuccess ? SdkPaymentStatus.SUCCESS : SdkPaymentStatus.FAILED,
|
|
258
|
+
orderId: data.orderId,
|
|
259
|
+
errorCode: data.error?.code ?? "",
|
|
260
|
+
errorMessage: data.error?.message ?? "",
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (data.transactionId) {
|
|
264
|
+
result.transactionId = data.transactionId;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (data.error?.referenceError) {
|
|
268
|
+
result.errorReference = data.error.referenceError;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (data.transaction) {
|
|
272
|
+
result.transaction = data.transaction as unknown as TransactionDto;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw new PaymentSDKError(
|
|
278
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
279
|
+
"Failed to verify final transaction status",
|
|
280
|
+
error,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async refundTransaction(
|
|
286
|
+
request: RefundRequest,
|
|
287
|
+
_onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
|
|
288
|
+
): Promise<PaymentResult> {
|
|
289
|
+
try {
|
|
290
|
+
const payload: CreateRefundDto = {
|
|
291
|
+
amount: request.amountCents,
|
|
292
|
+
businessId: Number(this.config.storeId),
|
|
293
|
+
displayId: this.config.kioskId,
|
|
294
|
+
currency: request.currency,
|
|
295
|
+
orderReferenceId: request.orderRef,
|
|
296
|
+
referenceId: request.originalTransactionId,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const { data } = await this.api.refundSingleVivaTransaction(payload);
|
|
300
|
+
|
|
301
|
+
const isSuccess = data.success;
|
|
302
|
+
|
|
303
|
+
const result: PaymentResult = {
|
|
304
|
+
success: isSuccess,
|
|
305
|
+
status: isSuccess ? SdkPaymentStatus.SUCCESS : SdkPaymentStatus.FAILED,
|
|
306
|
+
orderId: request.orderRef,
|
|
307
|
+
transactionId: data.sessionId,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return result;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw new PaymentSDKError(
|
|
313
|
+
PaymentErrorCode.NETWORK_ERROR,
|
|
314
|
+
"Failed to refund Viva transaction",
|
|
315
|
+
error,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
package/src/test-link.ts
ADDED
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export enum AppReaderStatus {
|
|
2
|
+
CONNECTING = "CONNECTING",
|
|
3
|
+
CONNECTED = "CONNECTED",
|
|
4
|
+
OFFLINE = "OFFLINE",
|
|
5
|
+
DISCONNECTED = "DISCONNECTED",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AppReader {
|
|
9
|
+
id: string;
|
|
10
|
+
serialNumber: string | null;
|
|
11
|
+
label: string;
|
|
12
|
+
status: AppReaderStatus;
|
|
13
|
+
batteryLevel?: number | null;
|
|
14
|
+
providerRawData?: unknown;
|
|
15
|
+
}
|