@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,532 @@
|
|
|
1
|
+
import { PaymentFailureCode, PaymentProvider } from "@munchi_oy/core";
|
|
2
|
+
import type { AxiosInstance } from "axios";
|
|
3
|
+
import { version } from "../package.json";
|
|
4
|
+
import { PaymentErrorCode, PaymentSDKError } from "./error";
|
|
5
|
+
import type { IPaymentStrategy } from "./strategies/IPaymentStrategy";
|
|
6
|
+
import { NetsStrategy } from "./strategies/NetsStrategy";
|
|
7
|
+
import { VivaStrategy } from "./strategies/VivaStrategy";
|
|
8
|
+
import {
|
|
9
|
+
type IMessagingAdapter,
|
|
10
|
+
type IMunchiPaymentSDK,
|
|
11
|
+
PaymentInteractionState,
|
|
12
|
+
type PaymentResult,
|
|
13
|
+
type PaymentTerminalConfig,
|
|
14
|
+
type RefundRequest,
|
|
15
|
+
type PaymentRequest as SdkPaymentRequest,
|
|
16
|
+
SdkPaymentStatus,
|
|
17
|
+
type TransactionOptions,
|
|
18
|
+
} from "./types/payment";
|
|
19
|
+
import type { ILogger, SDKOptions } from "./types/sdk";
|
|
20
|
+
|
|
21
|
+
type StateListener = (state: PaymentInteractionState) => void;
|
|
22
|
+
|
|
23
|
+
export class MunchiPaymentSDK implements IMunchiPaymentSDK {
|
|
24
|
+
private strategy: IPaymentStrategy;
|
|
25
|
+
private axios: AxiosInstance;
|
|
26
|
+
private messaging: IMessagingAdapter;
|
|
27
|
+
private timeoutMs: number;
|
|
28
|
+
private logger: ILogger | undefined;
|
|
29
|
+
private _currentState: PaymentInteractionState = PaymentInteractionState.IDLE;
|
|
30
|
+
private _listeners: StateListener[] = [];
|
|
31
|
+
private _cancellationIntent = false;
|
|
32
|
+
private _currentSessionId: string | undefined;
|
|
33
|
+
private _autoResetTimer: ReturnType<typeof setTimeout> | undefined;
|
|
34
|
+
private autoResetOptions: SDKOptions["autoResetOnPaymentComplete"];
|
|
35
|
+
|
|
36
|
+
private static readonly TERMINAL_STATES = [
|
|
37
|
+
PaymentInteractionState.SUCCESS,
|
|
38
|
+
PaymentInteractionState.FAILED,
|
|
39
|
+
PaymentInteractionState.INTERNAL_ERROR,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
private static readonly RESTING_STATES = [
|
|
43
|
+
PaymentInteractionState.IDLE,
|
|
44
|
+
...MunchiPaymentSDK.TERMINAL_STATES,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
axios: AxiosInstance,
|
|
49
|
+
messaging: IMessagingAdapter,
|
|
50
|
+
config: PaymentTerminalConfig,
|
|
51
|
+
options: SDKOptions = {},
|
|
52
|
+
strategy?: IPaymentStrategy,
|
|
53
|
+
) {
|
|
54
|
+
this.axios = axios;
|
|
55
|
+
this.messaging = messaging;
|
|
56
|
+
this.logger = options.logger;
|
|
57
|
+
this.timeoutMs = options.timeoutMs || 60000;
|
|
58
|
+
this.autoResetOptions = options.autoResetOnPaymentComplete;
|
|
59
|
+
this.strategy = strategy ?? this.resolveStrategy(config);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public get version() {
|
|
63
|
+
return version;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get currentState() {
|
|
67
|
+
return this._currentState;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private generateErrorResult(
|
|
71
|
+
orderRef: string,
|
|
72
|
+
code: PaymentErrorCode,
|
|
73
|
+
message: string,
|
|
74
|
+
): PaymentResult {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
status: SdkPaymentStatus.ERROR,
|
|
78
|
+
errorCode: this.normalizeErrorCode(code),
|
|
79
|
+
errorMessage: message,
|
|
80
|
+
orderId: orderRef,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private normalizeErrorCode(code?: string): string {
|
|
85
|
+
if (!code) return PaymentFailureCode.SystemUnknown;
|
|
86
|
+
if (code.includes(".")) return code;
|
|
87
|
+
|
|
88
|
+
const map: Record<string, string> = {
|
|
89
|
+
[PaymentErrorCode.CANCELLED]: PaymentFailureCode.PaymentCancelledByUser,
|
|
90
|
+
[PaymentErrorCode.DECLINED]: PaymentFailureCode.PaymentDeclined,
|
|
91
|
+
[PaymentErrorCode.TERMINAL_BUSY]: PaymentFailureCode.TerminalBusy,
|
|
92
|
+
[PaymentErrorCode.TERMINAL_OFFLINE]: PaymentFailureCode.TerminalOffline,
|
|
93
|
+
[PaymentErrorCode.TIMEOUT]: PaymentFailureCode.TerminalTimeout,
|
|
94
|
+
[PaymentErrorCode.NETWORK_ERROR]: PaymentFailureCode.SystemProviderError,
|
|
95
|
+
[PaymentErrorCode.STRATEGY_ERROR]: PaymentFailureCode.SystemProviderError,
|
|
96
|
+
[PaymentErrorCode.MISSING_CONFIG]: PaymentFailureCode.SystemUnknown,
|
|
97
|
+
[PaymentErrorCode.INVALID_AMOUNT]: PaymentFailureCode.SystemUnknown,
|
|
98
|
+
[PaymentErrorCode.UNKNOWN]: PaymentFailureCode.SystemUnknown,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return map[code] ?? PaymentFailureCode.SystemUnknown;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public subscribe = (listener: StateListener): (() => void) => {
|
|
105
|
+
this._listeners.push(listener);
|
|
106
|
+
listener(this._currentState);
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
this._listeners = this._listeners.filter((l) => l !== listener);
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
private transitionTo(newState: PaymentInteractionState) {
|
|
114
|
+
if (this._currentState === newState) return;
|
|
115
|
+
|
|
116
|
+
if (newState === PaymentInteractionState.IDLE) {
|
|
117
|
+
this.cancelAutoReset();
|
|
118
|
+
this._currentState = newState;
|
|
119
|
+
this._listeners.forEach((l) => l(newState));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const isOldStateTerminal = MunchiPaymentSDK.TERMINAL_STATES.includes(
|
|
124
|
+
this._currentState,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (isOldStateTerminal) {
|
|
128
|
+
const errorMsg = `Invalid State Transition: Attempted to move from terminal state ${this._currentState} to ${newState}`;
|
|
129
|
+
this.logger?.error(errorMsg);
|
|
130
|
+
|
|
131
|
+
if (this._currentState !== PaymentInteractionState.INTERNAL_ERROR) {
|
|
132
|
+
this._currentState = PaymentInteractionState.INTERNAL_ERROR;
|
|
133
|
+
this._listeners.forEach((l) => l(this._currentState));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new PaymentSDKError(PaymentErrorCode.UNKNOWN, errorMsg);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this._currentState = newState;
|
|
140
|
+
|
|
141
|
+
if (MunchiPaymentSDK.TERMINAL_STATES.includes(newState)) {
|
|
142
|
+
this.scheduleAutoReset(newState);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this._listeners.forEach((listener) => listener(newState));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private _resetScheduledAt: number | undefined;
|
|
149
|
+
|
|
150
|
+
public get nextAutoResetAt(): number | undefined {
|
|
151
|
+
return this._resetScheduledAt;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private cancelAutoReset() {
|
|
155
|
+
if (this._autoResetTimer) {
|
|
156
|
+
clearTimeout(this._autoResetTimer);
|
|
157
|
+
this._autoResetTimer = undefined;
|
|
158
|
+
}
|
|
159
|
+
this._resetScheduledAt = undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private scheduleAutoReset(state: PaymentInteractionState) {
|
|
163
|
+
// If not configured, auto-reset is DISABLED
|
|
164
|
+
if (!this.autoResetOptions) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const isSuccess = state === PaymentInteractionState.SUCCESS;
|
|
169
|
+
const delay = isSuccess
|
|
170
|
+
? (this.autoResetOptions.successDelayMs ?? 5000)
|
|
171
|
+
: (this.autoResetOptions.failureDelayMs ?? 5000);
|
|
172
|
+
|
|
173
|
+
this.logger?.info(`Scheduling auto-reset to IDLE in ${delay}ms`);
|
|
174
|
+
|
|
175
|
+
this._resetScheduledAt = Date.now() + delay;
|
|
176
|
+
|
|
177
|
+
this._autoResetTimer = setTimeout(() => {
|
|
178
|
+
this.logger?.info("Auto-reset triggered");
|
|
179
|
+
this.reset();
|
|
180
|
+
}, delay);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private resolveStrategy(config: PaymentTerminalConfig): IPaymentStrategy {
|
|
184
|
+
switch (config.provider) {
|
|
185
|
+
case PaymentProvider.Nets:
|
|
186
|
+
return new NetsStrategy(this.axios, this.messaging, config);
|
|
187
|
+
|
|
188
|
+
default:
|
|
189
|
+
return new VivaStrategy(this.axios, this.messaging, config);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public initiateTransaction = async (
|
|
194
|
+
params: SdkPaymentRequest,
|
|
195
|
+
options?: TransactionOptions,
|
|
196
|
+
): Promise<PaymentResult> => {
|
|
197
|
+
const callbacks = options ?? {};
|
|
198
|
+
const orderRef = params.orderRef;
|
|
199
|
+
|
|
200
|
+
const isRestingState = MunchiPaymentSDK.RESTING_STATES.includes(
|
|
201
|
+
this._currentState,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (!isRestingState) {
|
|
205
|
+
return this.generateErrorResult(
|
|
206
|
+
params.orderRef,
|
|
207
|
+
PaymentErrorCode.UNKNOWN,
|
|
208
|
+
"A transaction is already in progress",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const startTime = Date.now();
|
|
213
|
+
this._cancellationIntent = false;
|
|
214
|
+
// Prevent stale session IDs from previous transactions affecting new flows.
|
|
215
|
+
this._currentSessionId = undefined;
|
|
216
|
+
|
|
217
|
+
this.transitionTo(PaymentInteractionState.IDLE);
|
|
218
|
+
|
|
219
|
+
if (params.amountCents <= 0) {
|
|
220
|
+
return this.generateErrorResult(
|
|
221
|
+
params.orderRef,
|
|
222
|
+
PaymentErrorCode.INVALID_AMOUNT,
|
|
223
|
+
"Amount must be greater than 0",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const internalStateCallback = (
|
|
229
|
+
state: PaymentInteractionState,
|
|
230
|
+
detail?: { sessionId?: string },
|
|
231
|
+
) => {
|
|
232
|
+
if (detail?.sessionId) {
|
|
233
|
+
this._currentSessionId = detail.sessionId;
|
|
234
|
+
}
|
|
235
|
+
if (state !== PaymentInteractionState.FAILED) {
|
|
236
|
+
this.transitionTo(state);
|
|
237
|
+
this.fireStateCallback(state, callbacks, orderRef);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const transactionPromise = this.strategy.processPayment(
|
|
242
|
+
params,
|
|
243
|
+
internalStateCallback,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
reject(
|
|
249
|
+
new PaymentSDKError(
|
|
250
|
+
PaymentErrorCode.TIMEOUT,
|
|
251
|
+
"Transaction timed out",
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}, this.timeoutMs);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const result = await Promise.race([transactionPromise, timeoutPromise]);
|
|
258
|
+
|
|
259
|
+
if (result.success) {
|
|
260
|
+
this.transitionTo(PaymentInteractionState.SUCCESS);
|
|
261
|
+
this.safeFireCallback(() => callbacks.onSuccess?.(result));
|
|
262
|
+
} else if (this._cancellationIntent) {
|
|
263
|
+
return await this.handleTransactionError(
|
|
264
|
+
params,
|
|
265
|
+
new Error("Aborted after resolution"),
|
|
266
|
+
callbacks,
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
this.transitionTo(PaymentInteractionState.FAILED);
|
|
270
|
+
this.safeFireCallback(() => callbacks.onError?.(result));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.logger?.info("Transaction completed successfully", {
|
|
274
|
+
orderId: params.orderRef,
|
|
275
|
+
durationMs: Date.now() - startTime,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return result;
|
|
279
|
+
} catch (error: unknown) {
|
|
280
|
+
this.logger?.warn("Transaction interrupted. Handling final status...", {
|
|
281
|
+
error,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return await this.handleTransactionError(params, error, callbacks);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
private async handleTransactionError(
|
|
289
|
+
params: SdkPaymentRequest,
|
|
290
|
+
originalError: unknown,
|
|
291
|
+
callbacks: TransactionOptions = {},
|
|
292
|
+
): Promise<PaymentResult> {
|
|
293
|
+
if (!this._cancellationIntent) {
|
|
294
|
+
this.transitionTo(PaymentInteractionState.VERIFYING);
|
|
295
|
+
this.safeFireCallback(() =>
|
|
296
|
+
callbacks.onVerifying?.({
|
|
297
|
+
orderRef: params.orderRef,
|
|
298
|
+
refPaymentId: this._currentSessionId,
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (this._cancellationIntent) {
|
|
304
|
+
try {
|
|
305
|
+
if (this._currentSessionId) {
|
|
306
|
+
const finalStatus = await this.strategy.verifyFinalStatus(
|
|
307
|
+
params,
|
|
308
|
+
this._currentSessionId,
|
|
309
|
+
);
|
|
310
|
+
if (finalStatus.success) {
|
|
311
|
+
this.transitionTo(PaymentInteractionState.SUCCESS);
|
|
312
|
+
this.safeFireCallback(() => callbacks.onSuccess?.(finalStatus));
|
|
313
|
+
return finalStatus;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
this.logger?.warn(
|
|
318
|
+
"Final status verification failed during cancellation",
|
|
319
|
+
{ err },
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.transitionTo(PaymentInteractionState.FAILED);
|
|
324
|
+
// Removed cancelAutoReset() to allow the auto-reset timer (scheduled by FAILED state) to persist.
|
|
325
|
+
|
|
326
|
+
this.safeFireCallback(() =>
|
|
327
|
+
callbacks.onCancelled?.({
|
|
328
|
+
orderRef: params.orderRef,
|
|
329
|
+
refPaymentId: this._currentSessionId,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
status: SdkPaymentStatus.CANCELLED,
|
|
335
|
+
errorCode: this.normalizeErrorCode(PaymentErrorCode.CANCELLED),
|
|
336
|
+
orderId: params.orderRef,
|
|
337
|
+
...(this._currentSessionId
|
|
338
|
+
? { transactionId: this._currentSessionId }
|
|
339
|
+
: {}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
this.transitionTo(PaymentInteractionState.FAILED);
|
|
344
|
+
|
|
345
|
+
let errorResult: PaymentResult;
|
|
346
|
+
|
|
347
|
+
if (this._currentSessionId) {
|
|
348
|
+
try {
|
|
349
|
+
const finalStatus = await this.strategy.verifyFinalStatus(
|
|
350
|
+
params,
|
|
351
|
+
this._currentSessionId,
|
|
352
|
+
);
|
|
353
|
+
errorResult = finalStatus;
|
|
354
|
+
} catch (verifyErr) {
|
|
355
|
+
this.logger?.warn(
|
|
356
|
+
"Failed to get detailed error from verifyFinalStatus",
|
|
357
|
+
{ verifyErr },
|
|
358
|
+
);
|
|
359
|
+
errorResult = this.buildErrorResultFromException(
|
|
360
|
+
params.orderRef,
|
|
361
|
+
originalError,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
errorResult = this.buildErrorResultFromException(
|
|
366
|
+
params.orderRef,
|
|
367
|
+
originalError,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.safeFireCallback(() => callbacks.onError?.(errorResult));
|
|
372
|
+
return errorResult;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private buildErrorResultFromException(
|
|
376
|
+
orderRef: string,
|
|
377
|
+
error: unknown,
|
|
378
|
+
): PaymentResult {
|
|
379
|
+
return error instanceof PaymentSDKError
|
|
380
|
+
? this.generateErrorResult(orderRef, error.code, error.message)
|
|
381
|
+
: this.generateErrorResult(
|
|
382
|
+
orderRef,
|
|
383
|
+
PaymentErrorCode.UNKNOWN,
|
|
384
|
+
error instanceof Error ? error.message : "Unknown fatal error",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private fireStateCallback(
|
|
389
|
+
state: PaymentInteractionState,
|
|
390
|
+
callbacks: TransactionOptions,
|
|
391
|
+
orderRef: string,
|
|
392
|
+
) {
|
|
393
|
+
const ctx = {
|
|
394
|
+
orderRef,
|
|
395
|
+
refPaymentId: this._currentSessionId,
|
|
396
|
+
};
|
|
397
|
+
switch (state) {
|
|
398
|
+
case PaymentInteractionState.CONNECTING:
|
|
399
|
+
this.safeFireCallback(() => callbacks.onConnecting?.(ctx));
|
|
400
|
+
break;
|
|
401
|
+
case PaymentInteractionState.REQUIRES_INPUT:
|
|
402
|
+
this.safeFireCallback(() => callbacks.onRequiresInput?.(ctx));
|
|
403
|
+
break;
|
|
404
|
+
case PaymentInteractionState.PROCESSING:
|
|
405
|
+
this.safeFireCallback(() => callbacks.onProcessing?.(ctx));
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private safeFireCallback(callback: () => void) {
|
|
411
|
+
try {
|
|
412
|
+
callback();
|
|
413
|
+
} catch (error) {
|
|
414
|
+
this.logger?.warn("Callback execution failed", { error });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
public cancel = async (): Promise<boolean> => {
|
|
419
|
+
this.logger?.info("Attempting cancellation");
|
|
420
|
+
|
|
421
|
+
if (MunchiPaymentSDK.TERMINAL_STATES.includes(this._currentState)) {
|
|
422
|
+
this.logger?.warn(
|
|
423
|
+
"Cannot cancel: Transaction already in terminal state",
|
|
424
|
+
{
|
|
425
|
+
state: this._currentState,
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
this._cancellationIntent = true;
|
|
432
|
+
|
|
433
|
+
this.transitionTo(PaymentInteractionState.VERIFYING);
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const result = await this.strategy.cancelTransaction((state) =>
|
|
437
|
+
this.transitionTo(state),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// If cancellation failed (e.g. no active session) AND we are just verifying,
|
|
441
|
+
// we should probably revert to IDLE or FAILED to avoid getting stuck.
|
|
442
|
+
if (!result && this._currentState === PaymentInteractionState.VERIFYING) {
|
|
443
|
+
this.transitionTo(PaymentInteractionState.IDLE);
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
this.logger?.error("Cancellation command failed", error);
|
|
448
|
+
// Do NOT set internal error here.
|
|
449
|
+
// If cancellation failed, the main initiateTransaction loop will likely catch the error
|
|
450
|
+
// (or the abort signal) and handle the flow via handleTransactionError.
|
|
451
|
+
// Setting terminal state here causes a race condition.
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
public reset = (): void => {
|
|
457
|
+
if (MunchiPaymentSDK.TERMINAL_STATES.includes(this._currentState)) {
|
|
458
|
+
this.transitionTo(PaymentInteractionState.IDLE);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
public refund = async (
|
|
463
|
+
params: RefundRequest,
|
|
464
|
+
options?: TransactionOptions,
|
|
465
|
+
): Promise<PaymentResult> => {
|
|
466
|
+
const callbacks = options ?? {};
|
|
467
|
+
this.logger?.info("Initiating refund", { orderRef: params.orderRef });
|
|
468
|
+
// Clear any stale session before starting a new refund flow.
|
|
469
|
+
this._currentSessionId = undefined;
|
|
470
|
+
|
|
471
|
+
// Ensure we are in a valid state to start a refund
|
|
472
|
+
const isRestingState = MunchiPaymentSDK.RESTING_STATES.includes(
|
|
473
|
+
this._currentState,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
if (!isRestingState) {
|
|
477
|
+
return this.generateErrorResult(
|
|
478
|
+
params.orderRef,
|
|
479
|
+
PaymentErrorCode.UNKNOWN,
|
|
480
|
+
"A transaction is already in progress",
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.transitionTo(PaymentInteractionState.IDLE);
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const internalStateCallback = (
|
|
488
|
+
state: PaymentInteractionState,
|
|
489
|
+
detail?: { sessionId?: string },
|
|
490
|
+
) => {
|
|
491
|
+
if (detail?.sessionId) {
|
|
492
|
+
this._currentSessionId = detail.sessionId;
|
|
493
|
+
}
|
|
494
|
+
if (state !== PaymentInteractionState.FAILED) {
|
|
495
|
+
this.transitionTo(state);
|
|
496
|
+
this.fireStateCallback(state, callbacks, params.orderRef);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const result = await this.strategy.refundTransaction(
|
|
501
|
+
params,
|
|
502
|
+
internalStateCallback,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
if (result.success) {
|
|
506
|
+
this.transitionTo(PaymentInteractionState.SUCCESS);
|
|
507
|
+
this.safeFireCallback(() => callbacks.onSuccess?.(result));
|
|
508
|
+
} else {
|
|
509
|
+
this.transitionTo(PaymentInteractionState.FAILED);
|
|
510
|
+
this.safeFireCallback(() => callbacks.onError?.(result));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
this.logger?.info("Refund completed", {
|
|
514
|
+
success: result.success,
|
|
515
|
+
orderRef: params.orderRef,
|
|
516
|
+
});
|
|
517
|
+
return result;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
this.logger?.error("Refund failed", error);
|
|
520
|
+
|
|
521
|
+
this.transitionTo(PaymentInteractionState.FAILED);
|
|
522
|
+
|
|
523
|
+
const errorResult = this.generateErrorResult(
|
|
524
|
+
params.orderRef,
|
|
525
|
+
PaymentErrorCode.UNKNOWN,
|
|
526
|
+
error instanceof Error ? error.message : "Refund failed",
|
|
527
|
+
);
|
|
528
|
+
this.safeFireCallback(() => callbacks.onError?.(errorResult));
|
|
529
|
+
return errorResult;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export enum PaymentErrorCode {
|
|
2
|
+
MISSING_CONFIG = "MISSING_CONFIG",
|
|
3
|
+
INVALID_AMOUNT = "INVALID_AMOUNT",
|
|
4
|
+
NETWORK_ERROR = "NETWORK_ERROR",
|
|
5
|
+
TERMINAL_OFFLINE = "TERMINAL_OFFLINE",
|
|
6
|
+
TERMINAL_BUSY = "TERMINAL_BUSY",
|
|
7
|
+
TIMEOUT = "TIMEOUT",
|
|
8
|
+
DECLINED = "DECLINED",
|
|
9
|
+
CANCELLED = "CANCELLED",
|
|
10
|
+
STRATEGY_ERROR = "STRATEGY_ERROR",
|
|
11
|
+
UNKNOWN = "UNKNOWN",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class PaymentSDKError extends Error {
|
|
15
|
+
public readonly code: PaymentErrorCode;
|
|
16
|
+
public readonly rawError?: unknown;
|
|
17
|
+
|
|
18
|
+
constructor(code: PaymentErrorCode, message: string, rawError?: unknown) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "PaymentSDKError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.rawError = rawError;
|
|
23
|
+
|
|
24
|
+
Object.setPrototypeOf(this, PaymentSDKError.prototype);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PaymentInteractionState,
|
|
3
|
+
PaymentRequest,
|
|
4
|
+
PaymentResult,
|
|
5
|
+
RefundRequest,
|
|
6
|
+
} from "../types/payment";
|
|
7
|
+
|
|
8
|
+
export interface IPaymentStrategy {
|
|
9
|
+
|
|
10
|
+
processPayment(
|
|
11
|
+
request: PaymentRequest,
|
|
12
|
+
onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
|
|
13
|
+
): Promise<PaymentResult>;
|
|
14
|
+
cancelTransaction(
|
|
15
|
+
onStateChange: (state: PaymentInteractionState) => void,
|
|
16
|
+
): Promise<boolean>;
|
|
17
|
+
refundTransaction(
|
|
18
|
+
request: RefundRequest,
|
|
19
|
+
onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
|
|
20
|
+
): Promise<PaymentResult>;
|
|
21
|
+
verifyFinalStatus(request: PaymentRequest, sessionId: string): Promise<PaymentResult>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PaymentInteractionState,
|
|
3
|
+
type PaymentRequest,
|
|
4
|
+
type PaymentResult,
|
|
5
|
+
type RefundRequest,
|
|
6
|
+
SdkPaymentStatus,
|
|
7
|
+
} from "../types/payment";
|
|
8
|
+
import type { IPaymentStrategy } from "./IPaymentStrategy";
|
|
9
|
+
|
|
10
|
+
export class MockStrategy implements IPaymentStrategy {
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async processPayment(
|
|
14
|
+
request: PaymentRequest,
|
|
15
|
+
_onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
|
|
16
|
+
): Promise<PaymentResult> {
|
|
17
|
+
console.log("[MockSDK] Processing...", request);
|
|
18
|
+
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
status: SdkPaymentStatus.SUCCESS,
|
|
24
|
+
orderId: request.orderRef,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async cancelTransaction(
|
|
29
|
+
_onStateChange: (state: PaymentInteractionState) => void,
|
|
30
|
+
): Promise<boolean> {
|
|
31
|
+
console.log("[MockSDK] Cancelled");
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async verifyFinalStatus(request: PaymentRequest, sessionId: string): Promise<PaymentResult> {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
status: SdkPaymentStatus.FAILED,
|
|
39
|
+
orderId: request.orderRef,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async refundTransaction(
|
|
44
|
+
request: RefundRequest,
|
|
45
|
+
_onStateChange: (state: PaymentInteractionState, detail?: { sessionId?: string }) => void,
|
|
46
|
+
): Promise<PaymentResult> {
|
|
47
|
+
console.log("[MockSDK] Refunding...", request);
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
status: SdkPaymentStatus.SUCCESS,
|
|
52
|
+
orderId: request.orderRef,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|