@misterhomer1992/miit-bot-payment 1.1.6 → 2.0.4
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/dist/config/ConfigurationManager.d.ts +64 -0
- package/dist/config/ConfigurationManager.d.ts.map +1 -0
- package/dist/config/ConfigurationManager.js +144 -0
- package/dist/config/ConfigurationManager.js.map +1 -0
- package/dist/config/defaults.d.ts +18 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +26 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/environment.d.ts +38 -0
- package/dist/config/environment.d.ts.map +1 -0
- package/dist/config/environment.js +91 -0
- package/dist/config/environment.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +18 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +53 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +3 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/cache/InMemoryCache.d.ts +17 -0
- package/dist/modules/cache/InMemoryCache.d.ts.map +1 -0
- package/dist/modules/cache/InMemoryCache.js +77 -0
- package/dist/modules/cache/InMemoryCache.js.map +1 -0
- package/dist/modules/cache/index.d.ts +3 -0
- package/dist/modules/cache/index.d.ts.map +1 -0
- package/dist/modules/cache/index.js +19 -0
- package/dist/modules/cache/index.js.map +1 -0
- package/dist/modules/cache/types.d.ts +52 -0
- package/dist/modules/cache/types.d.ts.map +1 -0
- package/dist/modules/cache/types.js +3 -0
- package/dist/modules/cache/types.js.map +1 -0
- package/dist/modules/errors/index.d.ts +2 -0
- package/dist/modules/errors/index.d.ts.map +1 -0
- package/dist/modules/errors/index.js +19 -0
- package/dist/modules/errors/index.js.map +1 -0
- package/dist/modules/errors/types.d.ts +112 -0
- package/dist/modules/errors/types.d.ts.map +1 -0
- package/dist/modules/errors/types.js +174 -0
- package/dist/modules/errors/types.js.map +1 -0
- package/dist/modules/payments/api.d.ts +63 -1
- package/dist/modules/payments/api.d.ts.map +1 -1
- package/dist/modules/payments/api.js +103 -1
- package/dist/modules/payments/api.js.map +1 -1
- package/dist/modules/payments/const.d.ts.map +1 -1
- package/dist/modules/payments/const.js +1 -0
- package/dist/modules/payments/const.js.map +1 -1
- package/dist/modules/payments/index.d.ts +8 -0
- package/dist/modules/payments/index.d.ts.map +1 -1
- package/dist/modules/payments/index.js +8 -0
- package/dist/modules/payments/index.js.map +1 -1
- package/dist/modules/payments/service.d.ts +42 -2
- package/dist/modules/payments/service.d.ts.map +1 -1
- package/dist/modules/payments/service.js +132 -3
- package/dist/modules/payments/service.js.map +1 -1
- package/dist/modules/payments/subscription-check-webhook.handler.d.ts +85 -0
- package/dist/modules/payments/subscription-check-webhook.handler.d.ts.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.handler.js +155 -0
- package/dist/modules/payments/subscription-check-webhook.handler.js.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.service.d.ts +59 -0
- package/dist/modules/payments/subscription-check-webhook.service.d.ts.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.service.js +330 -0
- package/dist/modules/payments/subscription-check-webhook.service.js.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.types.d.ts +25 -0
- package/dist/modules/payments/subscription-check-webhook.types.d.ts.map +1 -0
- package/dist/modules/payments/subscription-check-webhook.types.js +3 -0
- package/dist/modules/payments/subscription-check-webhook.types.js.map +1 -0
- package/dist/modules/payments/types.d.ts +69 -2
- package/dist/modules/payments/types.d.ts.map +1 -1
- package/dist/modules/payments/utils.d.ts +151 -5
- package/dist/modules/payments/utils.d.ts.map +1 -1
- package/dist/modules/payments/utils.js +253 -9
- package/dist/modules/payments/utils.js.map +1 -1
- package/dist/modules/payments/wayforpay.service.d.ts +39 -0
- package/dist/modules/payments/wayforpay.service.d.ts.map +1 -0
- package/dist/modules/payments/wayforpay.service.js +217 -0
- package/dist/modules/payments/wayforpay.service.js.map +1 -0
- package/dist/modules/payments/wayforpay.types.d.ts +115 -0
- package/dist/modules/payments/wayforpay.types.d.ts.map +1 -0
- package/dist/modules/payments/wayforpay.types.js +3 -0
- package/dist/modules/payments/wayforpay.types.js.map +1 -0
- package/dist/modules/payments/webhook.handler.d.ts +98 -0
- package/dist/modules/payments/webhook.handler.d.ts.map +1 -0
- package/dist/modules/payments/webhook.handler.js +153 -0
- package/dist/modules/payments/webhook.handler.js.map +1 -0
- package/dist/modules/payments/webhook.service.d.ts +99 -0
- package/dist/modules/payments/webhook.service.d.ts.map +1 -0
- package/dist/modules/payments/webhook.service.js +672 -0
- package/dist/modules/payments/webhook.service.js.map +1 -0
- package/dist/modules/payments/webhook.types.d.ts +35 -0
- package/dist/modules/payments/webhook.types.d.ts.map +1 -0
- package/dist/modules/payments/webhook.types.js +3 -0
- package/dist/modules/payments/webhook.types.js.map +1 -0
- package/dist/modules/subscription/change.service.d.ts +80 -0
- package/dist/modules/subscription/change.service.d.ts.map +1 -0
- package/dist/modules/subscription/change.service.js +226 -0
- package/dist/modules/subscription/change.service.js.map +1 -0
- package/dist/modules/subscription/index.d.ts +2 -0
- package/dist/modules/subscription/index.d.ts.map +1 -1
- package/dist/modules/subscription/index.js +2 -0
- package/dist/modules/subscription/index.js.map +1 -1
- package/dist/modules/subscription/service.d.ts +8 -1
- package/dist/modules/subscription/service.d.ts.map +1 -1
- package/dist/modules/subscription/service.js +59 -2
- package/dist/modules/subscription/service.js.map +1 -1
- package/dist/modules/subscription/status-check.handler.d.ts +117 -0
- package/dist/modules/subscription/status-check.handler.d.ts.map +1 -0
- package/dist/modules/subscription/status-check.handler.js +164 -0
- package/dist/modules/subscription/status-check.handler.js.map +1 -0
- package/dist/modules/subscription/types.d.ts +37 -1
- package/dist/modules/subscription/types.d.ts.map +1 -1
- package/dist/modules/subscriptionPlan/const.d.ts +5 -0
- package/dist/modules/subscriptionPlan/const.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/const.js +106 -0
- package/dist/modules/subscriptionPlan/const.js.map +1 -0
- package/dist/modules/subscriptionPlan/index.d.ts +5 -0
- package/dist/modules/subscriptionPlan/index.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/index.js +21 -0
- package/dist/modules/subscriptionPlan/index.js.map +1 -0
- package/dist/modules/subscriptionPlan/repository.d.ts +22 -0
- package/dist/modules/subscriptionPlan/repository.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/repository.js +95 -0
- package/dist/modules/subscriptionPlan/repository.js.map +1 -0
- package/dist/modules/subscriptionPlan/service.d.ts +21 -0
- package/dist/modules/subscriptionPlan/service.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/service.js +128 -0
- package/dist/modules/subscriptionPlan/service.js.map +1 -0
- package/dist/modules/subscriptionPlan/types.d.ts +40 -0
- package/dist/modules/subscriptionPlan/types.d.ts.map +1 -0
- package/dist/modules/subscriptionPlan/types.js +3 -0
- package/dist/modules/subscriptionPlan/types.js.map +1 -0
- package/dist/modules/token/const.d.ts +7 -0
- package/dist/modules/token/const.d.ts.map +1 -0
- package/dist/modules/token/const.js +66 -0
- package/dist/modules/token/const.js.map +1 -0
- package/dist/modules/token/index.d.ts +4 -0
- package/dist/modules/token/index.d.ts.map +1 -0
- package/dist/modules/token/index.js +20 -0
- package/dist/modules/token/index.js.map +1 -0
- package/dist/modules/token/service.d.ts +46 -0
- package/dist/modules/token/service.d.ts.map +1 -0
- package/dist/modules/token/service.js +249 -0
- package/dist/modules/token/service.js.map +1 -0
- package/dist/modules/token/types.d.ts +109 -0
- package/dist/modules/token/types.d.ts.map +1 -0
- package/dist/modules/token/types.js +3 -0
- package/dist/modules/token/types.js.map +1 -0
- package/dist/modules/tokenPack/const.d.ts +4 -0
- package/dist/modules/tokenPack/const.d.ts.map +1 -0
- package/dist/modules/tokenPack/const.js +10 -0
- package/dist/modules/tokenPack/const.js.map +1 -0
- package/dist/modules/tokenPack/index.d.ts +5 -0
- package/dist/modules/tokenPack/index.d.ts.map +1 -0
- package/dist/modules/tokenPack/index.js +21 -0
- package/dist/modules/tokenPack/index.js.map +1 -0
- package/dist/modules/tokenPack/repository.d.ts +32 -0
- package/dist/modules/tokenPack/repository.d.ts.map +1 -0
- package/dist/modules/tokenPack/repository.js +103 -0
- package/dist/modules/tokenPack/repository.js.map +1 -0
- package/dist/modules/tokenPack/service.d.ts +28 -0
- package/dist/modules/tokenPack/service.d.ts.map +1 -0
- package/dist/modules/tokenPack/service.js +106 -0
- package/dist/modules/tokenPack/service.js.map +1 -0
- package/dist/modules/tokenPack/types.d.ts +124 -0
- package/dist/modules/tokenPack/types.d.ts.map +1 -0
- package/dist/modules/tokenPack/types.js +3 -0
- package/dist/modules/tokenPack/types.js.map +1 -0
- package/package.json +9 -5
- package/src/config/ConfigurationManager.ts +159 -0
- package/src/config/defaults.ts +27 -0
- package/src/config/environment.ts +94 -0
- package/src/config/index.ts +22 -0
- package/src/config/types.ts +56 -0
- package/src/index.ts +29 -0
- package/src/modules/cache/InMemoryCache.ts +98 -0
- package/src/modules/cache/index.ts +2 -0
- package/src/modules/cache/types.ts +60 -0
- package/src/modules/cancellableAPI/utils.ts +60 -0
- package/src/modules/errors/index.ts +16 -0
- package/src/modules/errors/types.ts +201 -0
- package/src/modules/invoice/const.ts +7 -0
- package/src/modules/invoice/index.ts +4 -0
- package/src/modules/invoice/repository.ts +52 -0
- package/src/modules/invoice/service.ts +44 -0
- package/src/modules/invoice/types.ts +47 -0
- package/src/modules/logger/types.ts +8 -0
- package/src/modules/network/utils.ts +24 -0
- package/src/modules/payments/api.ts +289 -0
- package/src/modules/payments/const.ts +11 -0
- package/src/modules/payments/index.ts +14 -0
- package/src/modules/payments/repository.ts +125 -0
- package/src/modules/payments/service.test.ts +400 -0
- package/src/modules/payments/service.ts +365 -0
- package/src/modules/payments/subscription-check-webhook.handler.integration.test.ts +935 -0
- package/src/modules/payments/subscription-check-webhook.handler.ts +211 -0
- package/src/modules/payments/subscription-check-webhook.service.ts +398 -0
- package/src/modules/payments/subscription-check-webhook.types.ts +29 -0
- package/src/modules/payments/types.ts +193 -0
- package/src/modules/payments/utils.ts +428 -0
- package/src/modules/payments/wayforpay.service.test.ts +375 -0
- package/src/modules/payments/wayforpay.service.ts +284 -0
- package/src/modules/payments/wayforpay.types.ts +138 -0
- package/src/modules/payments/webhook.handler.integration.test.ts +975 -0
- package/src/modules/payments/webhook.handler.ts +219 -0
- package/src/modules/payments/webhook.service.ts +812 -0
- package/src/modules/payments/webhook.types.ts +38 -0
- package/src/modules/subscription/change.service.ts +317 -0
- package/src/modules/subscription/const.ts +9 -0
- package/src/modules/subscription/index.ts +5 -0
- package/src/modules/subscription/repository.ts +277 -0
- package/src/modules/subscription/service.test.ts +665 -0
- package/src/modules/subscription/service.ts +328 -0
- package/src/modules/subscription/status-check.handler.ts +254 -0
- package/src/modules/subscription/types.ts +267 -0
- package/src/modules/subscription/utils.ts +5 -0
- package/src/modules/subscriptionPlan/const.ts +106 -0
- package/src/modules/subscriptionPlan/index.ts +4 -0
- package/src/modules/subscriptionPlan/repository.ts +129 -0
- package/src/modules/subscriptionPlan/service.test.ts +401 -0
- package/src/modules/subscriptionPlan/service.ts +148 -0
- package/src/modules/subscriptionPlan/types.ts +67 -0
- package/src/modules/token/const.ts +64 -0
- package/src/modules/token/index.ts +3 -0
- package/src/modules/token/service.test.ts +499 -0
- package/src/modules/token/service.ts +297 -0
- package/src/modules/token/types.ts +124 -0
- package/src/modules/tokenPack/const.ts +9 -0
- package/src/modules/tokenPack/index.ts +4 -0
- package/src/modules/tokenPack/repository.ts +144 -0
- package/src/modules/tokenPack/service.ts +119 -0
- package/src/modules/tokenPack/types.ts +131 -0
- package/src/modules/user/index.ts +3 -0
- package/src/modules/user/types.ts +143 -0
- package/src/modules/user/userRepository.ts +64 -0
- package/src/modules/user/userService.ts +68 -0
- package/src/types/extend-express.d.ts +16 -0
- package/src/types/function.ts +5 -0
- package/src/types/utilities.ts +22 -0
- package/src/utils.ts +53 -0
- package/tsconfig.json +29 -0
- package/dist/modules/subscription/subscriptionPlan.d.ts +0 -4
- package/dist/modules/subscription/subscriptionPlan.d.ts.map +0 -1
- package/dist/modules/subscription/subscriptionPlan.js +0 -67
- package/dist/modules/subscription/subscriptionPlan.js.map +0 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { describe, it, beforeEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { WayForPayService } from './wayforpay.service';
|
|
5
|
+
import type { WayForPayCallbackData } from './wayforpay.types';
|
|
6
|
+
import type { Logger } from '../logger/types';
|
|
7
|
+
|
|
8
|
+
// Mock logger
|
|
9
|
+
const createMockLogger = (): Logger => ({
|
|
10
|
+
info: mock.fn(),
|
|
11
|
+
warning: mock.fn(),
|
|
12
|
+
error: mock.fn(),
|
|
13
|
+
debug: mock.fn(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Mock callback data
|
|
17
|
+
const createMockCallbackData = (
|
|
18
|
+
overrides: Partial<WayForPayCallbackData> = {},
|
|
19
|
+
secretKey: string = 'test-secret-key',
|
|
20
|
+
): WayForPayCallbackData => {
|
|
21
|
+
const baseData = {
|
|
22
|
+
merchantAccount: 'test_merchant',
|
|
23
|
+
orderReference: 'order-123',
|
|
24
|
+
amount: 100,
|
|
25
|
+
currency: 'UAH',
|
|
26
|
+
authCode: 'auth-123',
|
|
27
|
+
cardPan: '4111****1111',
|
|
28
|
+
transactionStatus: 'Approved',
|
|
29
|
+
reasonCode: 1100,
|
|
30
|
+
email: 'test@example.com',
|
|
31
|
+
phone: '+380123456789',
|
|
32
|
+
createdDate: Date.now(),
|
|
33
|
+
processingDate: Date.now(),
|
|
34
|
+
cardType: 'VISA',
|
|
35
|
+
issuerBankCountry: 'UA',
|
|
36
|
+
issuerBankName: 'Test Bank',
|
|
37
|
+
recToken: 'rec-token-123',
|
|
38
|
+
reason: 'Ok',
|
|
39
|
+
fee: 2.5,
|
|
40
|
+
paymentSystem: 'VISA',
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Calculate the correct signature
|
|
45
|
+
const signatureData = [
|
|
46
|
+
baseData.merchantAccount,
|
|
47
|
+
baseData.orderReference,
|
|
48
|
+
baseData.amount,
|
|
49
|
+
baseData.currency,
|
|
50
|
+
baseData.authCode,
|
|
51
|
+
baseData.cardPan,
|
|
52
|
+
baseData.transactionStatus,
|
|
53
|
+
baseData.reasonCode,
|
|
54
|
+
];
|
|
55
|
+
const signatureString = signatureData.join(';');
|
|
56
|
+
const merchantSignature = crypto.createHmac('md5', secretKey).update(signatureString).digest('hex');
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...baseData,
|
|
60
|
+
merchantSignature: overrides.merchantSignature !== undefined ? overrides.merchantSignature : merchantSignature,
|
|
61
|
+
} as WayForPayCallbackData;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe('WayForPayService', () => {
|
|
65
|
+
let logger: Logger;
|
|
66
|
+
let service: WayForPayService;
|
|
67
|
+
const merchantAccount = 'test_merchant';
|
|
68
|
+
const merchantSecretKey = 'test-secret-key';
|
|
69
|
+
const merchantDomainName = 'example.com';
|
|
70
|
+
const serviceUrl = 'https://webhook.example.com/wayforpay';
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
logger = createMockLogger();
|
|
74
|
+
service = new WayForPayService({
|
|
75
|
+
logger,
|
|
76
|
+
merchantAccount,
|
|
77
|
+
merchantSecretKey,
|
|
78
|
+
merchantDomainName,
|
|
79
|
+
serviceUrl,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('constructor', () => {
|
|
84
|
+
it('should use provided config values', () => {
|
|
85
|
+
// The service stores these internally, we can verify behavior through other methods
|
|
86
|
+
const callbackData = createMockCallbackData({}, merchantSecretKey);
|
|
87
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
88
|
+
|
|
89
|
+
assert.strictEqual(result.isValid, true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle missing env vars gracefully in constructor', () => {
|
|
93
|
+
// Creating service without explicit config should not throw
|
|
94
|
+
const serviceWithoutConfig = new WayForPayService({ logger });
|
|
95
|
+
// It will use process.env values which may be undefined
|
|
96
|
+
assert.ok(serviceWithoutConfig);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('verifyCallbackSignature', () => {
|
|
101
|
+
it('should return valid for correctly signed callback', () => {
|
|
102
|
+
const callbackData = createMockCallbackData({}, merchantSecretKey);
|
|
103
|
+
|
|
104
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(result.isValid, true);
|
|
107
|
+
assert.strictEqual(result.error, undefined);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return invalid for missing signature', () => {
|
|
111
|
+
const callbackData = createMockCallbackData({ merchantSignature: '' }, merchantSecretKey);
|
|
112
|
+
|
|
113
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
114
|
+
|
|
115
|
+
assert.strictEqual(result.isValid, false);
|
|
116
|
+
assert.strictEqual(result.error, 'Missing merchant signature in callback data');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return invalid for incorrect signature', () => {
|
|
120
|
+
const callbackData = createMockCallbackData({}, merchantSecretKey);
|
|
121
|
+
callbackData.merchantSignature = 'invalid-signature';
|
|
122
|
+
|
|
123
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(result.isValid, false);
|
|
126
|
+
assert.strictEqual(result.error, undefined); // No error message, just warning logged
|
|
127
|
+
assert.strictEqual((logger.warning as any).mock.calls.length, 1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should be case-insensitive for signature comparison', () => {
|
|
131
|
+
const callbackData = createMockCallbackData({}, merchantSecretKey);
|
|
132
|
+
// Make signature uppercase
|
|
133
|
+
callbackData.merchantSignature = callbackData.merchantSignature.toUpperCase();
|
|
134
|
+
|
|
135
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(result.isValid, true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return error on exception', () => {
|
|
141
|
+
// Create callback with invalid data that might cause an error
|
|
142
|
+
const callbackData = null as unknown as WayForPayCallbackData;
|
|
143
|
+
|
|
144
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
145
|
+
|
|
146
|
+
assert.strictEqual(result.isValid, false);
|
|
147
|
+
assert.strictEqual(result.error, 'Error verifying signature');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should verify signature with different amounts', () => {
|
|
151
|
+
const callbackData = createMockCallbackData({ amount: 999.99 }, merchantSecretKey);
|
|
152
|
+
|
|
153
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
154
|
+
|
|
155
|
+
assert.strictEqual(result.isValid, true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should verify signature with different currencies', () => {
|
|
159
|
+
const callbackData = createMockCallbackData({ currency: 'USD' as any }, merchantSecretKey);
|
|
160
|
+
|
|
161
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
162
|
+
|
|
163
|
+
assert.strictEqual(result.isValid, true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('buildWebhookResponse', () => {
|
|
168
|
+
it('should build accept response with correct structure', () => {
|
|
169
|
+
const orderReference = 'order-123';
|
|
170
|
+
|
|
171
|
+
const result = service.buildWebhookResponse(orderReference, 'accept');
|
|
172
|
+
|
|
173
|
+
assert.strictEqual(result.orderReference, orderReference);
|
|
174
|
+
assert.strictEqual(result.status, 'accept');
|
|
175
|
+
assert.ok(typeof result.time === 'number');
|
|
176
|
+
assert.ok(typeof result.signature === 'string');
|
|
177
|
+
assert.ok(result.signature.length > 0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should build decline response', () => {
|
|
181
|
+
const orderReference = 'order-456';
|
|
182
|
+
|
|
183
|
+
const result = service.buildWebhookResponse(orderReference, 'decline');
|
|
184
|
+
|
|
185
|
+
assert.strictEqual(result.orderReference, orderReference);
|
|
186
|
+
assert.strictEqual(result.status, 'decline');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should generate unique timestamps', async () => {
|
|
190
|
+
const result1 = service.buildWebhookResponse('order-1', 'accept');
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
192
|
+
const result2 = service.buildWebhookResponse('order-2', 'accept');
|
|
193
|
+
|
|
194
|
+
// Timestamps should be different (or very close but result in different signatures)
|
|
195
|
+
assert.ok(result2.time >= result1.time);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should generate valid signature for response', () => {
|
|
199
|
+
const orderReference = 'order-123';
|
|
200
|
+
const status = 'accept';
|
|
201
|
+
|
|
202
|
+
const result = service.buildWebhookResponse(orderReference, status);
|
|
203
|
+
|
|
204
|
+
// Manually verify the signature
|
|
205
|
+
const signatureData = [orderReference, status, result.time];
|
|
206
|
+
const signatureString = signatureData.join(';');
|
|
207
|
+
const expectedSignature = crypto.createHmac('md5', merchantSecretKey).update(signatureString).digest('hex');
|
|
208
|
+
|
|
209
|
+
assert.strictEqual(result.signature, expectedSignature);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('generateSubscriptionPaymentUrl', () => {
|
|
214
|
+
it('should call the API with correct parameters', async () => {
|
|
215
|
+
// This method relies on external API (generateRegularPurchase from wayforpay-api)
|
|
216
|
+
// Full integration testing would require mocking the external module
|
|
217
|
+
// Here we test that the method exists and handles errors gracefully
|
|
218
|
+
|
|
219
|
+
const params = {
|
|
220
|
+
userId: 'user-123',
|
|
221
|
+
platform: 'telegram',
|
|
222
|
+
planId: 'plan-monthly',
|
|
223
|
+
productName: 'Monthly Subscription',
|
|
224
|
+
productPrice: 100,
|
|
225
|
+
currency: 'UAH' as const,
|
|
226
|
+
regularCount: 3,
|
|
227
|
+
regularMode: 'monthly' as const,
|
|
228
|
+
language: 'UA' as const,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Note: This will likely fail because it calls the actual API
|
|
232
|
+
// In a real test environment, we would mock the wayforpay-api module
|
|
233
|
+
try {
|
|
234
|
+
const result = await service.generateSubscriptionPaymentUrl(params);
|
|
235
|
+
// If API is available, verify structure
|
|
236
|
+
if (result) {
|
|
237
|
+
assert.ok(typeof result.url === 'string');
|
|
238
|
+
assert.ok(typeof result.orderReference === 'string');
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Expected to fail without proper API credentials
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should return null on API error', async () => {
|
|
246
|
+
// The method should return null and log error when API fails
|
|
247
|
+
const params = {
|
|
248
|
+
userId: 'user-123',
|
|
249
|
+
platform: 'telegram',
|
|
250
|
+
planId: 'plan-monthly',
|
|
251
|
+
productName: 'Monthly Subscription',
|
|
252
|
+
productPrice: 100,
|
|
253
|
+
currency: 'UAH' as const,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Create service with invalid credentials to trigger error
|
|
257
|
+
const invalidService = new WayForPayService({
|
|
258
|
+
logger,
|
|
259
|
+
merchantAccount: '',
|
|
260
|
+
merchantSecretKey: '',
|
|
261
|
+
merchantDomainName: '',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result = await invalidService.generateSubscriptionPaymentUrl(params);
|
|
265
|
+
|
|
266
|
+
// Should return null on error (API would fail)
|
|
267
|
+
assert.strictEqual(result, null);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('generateOneTimePaymentUrl', () => {
|
|
272
|
+
it('should call the API with correct parameters', async () => {
|
|
273
|
+
const params = {
|
|
274
|
+
userId: 'user-123',
|
|
275
|
+
platform: 'telegram',
|
|
276
|
+
productId: 'token-pack-small',
|
|
277
|
+
productName: 'Token Pack - 10000 tokens',
|
|
278
|
+
productPrice: 49,
|
|
279
|
+
currency: 'UAH' as const,
|
|
280
|
+
language: 'UA' as const,
|
|
281
|
+
returnUrl: 'https://example.com/return',
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Note: This will likely fail because it calls the actual API
|
|
285
|
+
try {
|
|
286
|
+
const result = await service.generateOneTimePaymentUrl(params);
|
|
287
|
+
if (result) {
|
|
288
|
+
assert.ok(typeof result.url === 'string');
|
|
289
|
+
assert.ok(typeof result.orderReference === 'string');
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// Expected to fail without proper API credentials
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should return null on API error', async () => {
|
|
297
|
+
const params = {
|
|
298
|
+
userId: 'user-123',
|
|
299
|
+
platform: 'telegram',
|
|
300
|
+
productId: 'token-pack-small',
|
|
301
|
+
productName: 'Token Pack',
|
|
302
|
+
productPrice: 49,
|
|
303
|
+
currency: 'UAH' as const,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Create service with invalid credentials to trigger error
|
|
307
|
+
const invalidService = new WayForPayService({
|
|
308
|
+
logger,
|
|
309
|
+
merchantAccount: '',
|
|
310
|
+
merchantSecretKey: '',
|
|
311
|
+
merchantDomainName: '',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result = await invalidService.generateOneTimePaymentUrl(params);
|
|
315
|
+
|
|
316
|
+
// Should return null on error
|
|
317
|
+
assert.strictEqual(result, null);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('signature calculation', () => {
|
|
322
|
+
it('should handle special characters in signature data', () => {
|
|
323
|
+
const callbackData = createMockCallbackData(
|
|
324
|
+
{
|
|
325
|
+
orderReference: 'order-with-special-chars-&-symbols',
|
|
326
|
+
},
|
|
327
|
+
merchantSecretKey,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
331
|
+
|
|
332
|
+
assert.strictEqual(result.isValid, true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should handle numeric reasonCode', () => {
|
|
336
|
+
const callbackData = createMockCallbackData(
|
|
337
|
+
{
|
|
338
|
+
reasonCode: '1100' as any,
|
|
339
|
+
},
|
|
340
|
+
merchantSecretKey,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
344
|
+
|
|
345
|
+
assert.strictEqual(result.isValid, true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should handle string reasonCode', () => {
|
|
349
|
+
const callbackData = createMockCallbackData(
|
|
350
|
+
{
|
|
351
|
+
reasonCode: '1100' as any,
|
|
352
|
+
},
|
|
353
|
+
merchantSecretKey,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Recalculate signature with string reasonCode
|
|
357
|
+
const signatureData = [
|
|
358
|
+
callbackData.merchantAccount,
|
|
359
|
+
callbackData.orderReference,
|
|
360
|
+
callbackData.amount,
|
|
361
|
+
callbackData.currency,
|
|
362
|
+
callbackData.authCode,
|
|
363
|
+
callbackData.cardPan,
|
|
364
|
+
callbackData.transactionStatus,
|
|
365
|
+
'1100',
|
|
366
|
+
];
|
|
367
|
+
const signatureString = signatureData.join(';');
|
|
368
|
+
callbackData.merchantSignature = crypto.createHmac('md5', merchantSecretKey).update(signatureString).digest('hex');
|
|
369
|
+
|
|
370
|
+
const result = service.verifyCallbackSignature(callbackData);
|
|
371
|
+
|
|
372
|
+
assert.strictEqual(result.isValid, true);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import moment from 'moment';
|
|
3
|
+
import { generateRegularPurchase, generatePurchase, Currency, Language } from '@misterhomer1992/wayforpay-api';
|
|
4
|
+
import { Logger } from '../logger/types';
|
|
5
|
+
import { isObject, isBadOrEmptyString } from '../../utils';
|
|
6
|
+
import { createSubscriptionOrderReference } from './utils';
|
|
7
|
+
import type {
|
|
8
|
+
IWayForPayService,
|
|
9
|
+
GenerateSubscriptionPaymentParams,
|
|
10
|
+
GenerateOneTimePaymentParams,
|
|
11
|
+
PaymentUrlResult,
|
|
12
|
+
SignatureVerificationResult,
|
|
13
|
+
WayForPayCallbackData,
|
|
14
|
+
WayForPayWebhookResponse,
|
|
15
|
+
} from './wayforpay.types';
|
|
16
|
+
|
|
17
|
+
const SIGNATURE_DELIMITER = ';';
|
|
18
|
+
|
|
19
|
+
interface WayforpayResponse {
|
|
20
|
+
url?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* WayForPayService provides integration with the WayForPay payment provider.
|
|
26
|
+
* Supports both subscription (regular) and one-time payment flows.
|
|
27
|
+
*/
|
|
28
|
+
export class WayForPayService implements IWayForPayService {
|
|
29
|
+
private readonly logger: Logger;
|
|
30
|
+
private readonly merchantAccount: string;
|
|
31
|
+
private readonly merchantSecretKey: string;
|
|
32
|
+
private readonly merchantDomainName: string;
|
|
33
|
+
private readonly serviceUrl?: string;
|
|
34
|
+
|
|
35
|
+
constructor({
|
|
36
|
+
logger,
|
|
37
|
+
merchantAccount,
|
|
38
|
+
merchantSecretKey,
|
|
39
|
+
merchantDomainName,
|
|
40
|
+
serviceUrl,
|
|
41
|
+
}: {
|
|
42
|
+
logger: Logger;
|
|
43
|
+
merchantAccount?: string;
|
|
44
|
+
merchantSecretKey?: string;
|
|
45
|
+
merchantDomainName?: string;
|
|
46
|
+
serviceUrl?: string;
|
|
47
|
+
}) {
|
|
48
|
+
this.logger = logger;
|
|
49
|
+
this.merchantAccount = merchantAccount || (process.env.WAYFORPAY_MERCHANT_ACCOUNT as string);
|
|
50
|
+
this.merchantSecretKey = merchantSecretKey || (process.env.WAYFORPAY_MERCHANT_SECRET_KEY as string);
|
|
51
|
+
this.merchantDomainName = merchantDomainName || (process.env.WAYFORPAY_MERCHANT_DOMAIN as string);
|
|
52
|
+
this.serviceUrl = serviceUrl || process.env.WAYFORPAY_SERVICE_URL;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generates a payment URL for subscription (regular) payments.
|
|
57
|
+
*/
|
|
58
|
+
public async generateSubscriptionPaymentUrl(
|
|
59
|
+
params: GenerateSubscriptionPaymentParams,
|
|
60
|
+
): Promise<PaymentUrlResult | null> {
|
|
61
|
+
const {
|
|
62
|
+
userId,
|
|
63
|
+
platform,
|
|
64
|
+
planId,
|
|
65
|
+
productName,
|
|
66
|
+
productPrice,
|
|
67
|
+
currency,
|
|
68
|
+
regularCount = 3,
|
|
69
|
+
regularMode = 'daily',
|
|
70
|
+
language = 'UA',
|
|
71
|
+
dateNext,
|
|
72
|
+
} = params;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const orderDate = Date.now();
|
|
76
|
+
const formattedUtc = moment.utc().toISOString();
|
|
77
|
+
const orderReference = createSubscriptionOrderReference({
|
|
78
|
+
userId,
|
|
79
|
+
platform,
|
|
80
|
+
planId,
|
|
81
|
+
utcDate: formattedUtc,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const response = (await generateRegularPurchase({
|
|
85
|
+
merchantAccount: this.merchantAccount,
|
|
86
|
+
merchantDomainName: this.merchantDomainName,
|
|
87
|
+
merchantSecretKey: this.merchantSecretKey,
|
|
88
|
+
orderDate,
|
|
89
|
+
orderReference,
|
|
90
|
+
productName: [productName],
|
|
91
|
+
productPrice: [productPrice],
|
|
92
|
+
currency: currency as Currency,
|
|
93
|
+
language: language as Language,
|
|
94
|
+
serviceUrl: this.serviceUrl,
|
|
95
|
+
regularMode,
|
|
96
|
+
regularCount,
|
|
97
|
+
dateNext,
|
|
98
|
+
})) as WayforpayResponse;
|
|
99
|
+
|
|
100
|
+
if (!isObject(response) || isBadOrEmptyString(response.url)) {
|
|
101
|
+
this.logger.error({
|
|
102
|
+
message: 'Invalid response from WayForPay generateRegularPurchase',
|
|
103
|
+
payload: {
|
|
104
|
+
orderReference,
|
|
105
|
+
response: JSON.stringify(response),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
url: response.url,
|
|
113
|
+
orderReference,
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
this.logger.error({
|
|
117
|
+
message: 'Error in WayForPayService generateSubscriptionPaymentUrl',
|
|
118
|
+
payload: {
|
|
119
|
+
userId,
|
|
120
|
+
platform,
|
|
121
|
+
planId,
|
|
122
|
+
productName,
|
|
123
|
+
productPrice,
|
|
124
|
+
currency,
|
|
125
|
+
error: JSON.stringify(error),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generates a payment URL for one-time payments.
|
|
134
|
+
*/
|
|
135
|
+
public async generateOneTimePaymentUrl(params: GenerateOneTimePaymentParams): Promise<PaymentUrlResult | null> {
|
|
136
|
+
const { userId, platform, productId, productName, productPrice, currency, language = 'UA', returnUrl } = params;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const orderDate = Date.now();
|
|
140
|
+
const formattedUtc = moment.utc().toISOString();
|
|
141
|
+
const orderReference = createSubscriptionOrderReference({
|
|
142
|
+
userId,
|
|
143
|
+
platform,
|
|
144
|
+
planId: productId,
|
|
145
|
+
utcDate: formattedUtc,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const response = (await generatePurchase({
|
|
149
|
+
merchantAccount: this.merchantAccount,
|
|
150
|
+
merchantDomainName: this.merchantDomainName,
|
|
151
|
+
merchantSecretKey: this.merchantSecretKey,
|
|
152
|
+
orderDate,
|
|
153
|
+
orderReference,
|
|
154
|
+
productName: [productName],
|
|
155
|
+
productPrice: [productPrice],
|
|
156
|
+
currency: currency as Currency,
|
|
157
|
+
language: language as Language,
|
|
158
|
+
serviceUrl: this.serviceUrl,
|
|
159
|
+
returnUrl,
|
|
160
|
+
})) as WayforpayResponse;
|
|
161
|
+
|
|
162
|
+
if (!isObject(response) || isBadOrEmptyString(response.url)) {
|
|
163
|
+
this.logger.error({
|
|
164
|
+
message: 'Invalid response from WayForPay generatePurchase',
|
|
165
|
+
payload: {
|
|
166
|
+
orderReference,
|
|
167
|
+
response: JSON.stringify(response),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
url: response.url,
|
|
175
|
+
orderReference,
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.logger.error({
|
|
179
|
+
message: 'Error in WayForPayService generateOneTimePaymentUrl',
|
|
180
|
+
payload: {
|
|
181
|
+
userId,
|
|
182
|
+
platform,
|
|
183
|
+
productId,
|
|
184
|
+
productName,
|
|
185
|
+
productPrice,
|
|
186
|
+
currency,
|
|
187
|
+
error: JSON.stringify(error),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Verifies the signature of a WayForPay webhook callback.
|
|
196
|
+
* The signature is calculated as HMAC_MD5 of concatenated values.
|
|
197
|
+
*/
|
|
198
|
+
public verifyCallbackSignature(callbackData: WayForPayCallbackData): SignatureVerificationResult {
|
|
199
|
+
try {
|
|
200
|
+
const {
|
|
201
|
+
merchantAccount,
|
|
202
|
+
orderReference,
|
|
203
|
+
amount,
|
|
204
|
+
currency,
|
|
205
|
+
authCode,
|
|
206
|
+
cardPan,
|
|
207
|
+
transactionStatus,
|
|
208
|
+
reasonCode,
|
|
209
|
+
merchantSignature,
|
|
210
|
+
} = callbackData;
|
|
211
|
+
|
|
212
|
+
if (!merchantSignature) {
|
|
213
|
+
return {
|
|
214
|
+
isValid: false,
|
|
215
|
+
error: 'Missing merchant signature in callback data',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// WayForPay signature is calculated from these fields in this exact order
|
|
220
|
+
const signatureData = [
|
|
221
|
+
merchantAccount,
|
|
222
|
+
orderReference,
|
|
223
|
+
amount,
|
|
224
|
+
currency,
|
|
225
|
+
authCode,
|
|
226
|
+
cardPan,
|
|
227
|
+
transactionStatus,
|
|
228
|
+
reasonCode,
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const signatureString = signatureData.join(SIGNATURE_DELIMITER);
|
|
232
|
+
const calculatedSignature = crypto
|
|
233
|
+
.createHmac('md5', this.merchantSecretKey)
|
|
234
|
+
.update(signatureString)
|
|
235
|
+
.digest('hex');
|
|
236
|
+
|
|
237
|
+
const isValid = calculatedSignature.toLowerCase() === merchantSignature.toLowerCase();
|
|
238
|
+
|
|
239
|
+
if (!isValid) {
|
|
240
|
+
this.logger.warning({
|
|
241
|
+
message: 'WayForPay signature verification failed',
|
|
242
|
+
payload: {
|
|
243
|
+
orderReference,
|
|
244
|
+
expectedSignature: calculatedSignature,
|
|
245
|
+
receivedSignature: merchantSignature,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { isValid };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.logger.error({
|
|
253
|
+
message: 'Error in WayForPayService verifyCallbackSignature',
|
|
254
|
+
payload: {
|
|
255
|
+
orderReference: callbackData?.orderReference,
|
|
256
|
+
error: JSON.stringify(error),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
isValid: false,
|
|
261
|
+
error: 'Error verifying signature',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Builds a response to send back to WayForPay after processing a webhook.
|
|
268
|
+
* WayForPay expects a signed response to confirm receipt.
|
|
269
|
+
*/
|
|
270
|
+
public buildWebhookResponse(orderReference: string, status: 'accept' | 'decline'): WayForPayWebhookResponse {
|
|
271
|
+
const time = Date.now();
|
|
272
|
+
|
|
273
|
+
const signatureData = [orderReference, status, time];
|
|
274
|
+
const signatureString = signatureData.join(SIGNATURE_DELIMITER);
|
|
275
|
+
const signature = crypto.createHmac('md5', this.merchantSecretKey).update(signatureString).digest('hex');
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
orderReference,
|
|
279
|
+
status,
|
|
280
|
+
time,
|
|
281
|
+
signature,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|