@misterhomer1992/miit-bot-payment 1.1.7 → 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 +58 -1
- 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,975 @@
|
|
|
1
|
+
import { describe, it, beforeEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { WebhookHandler } from './webhook.handler';
|
|
5
|
+
import { WebhookHandlerService } from './webhook.service';
|
|
6
|
+
import type { WayForPayCallbackData, IWayForPayService, WayForPayWebhookResponse } from './wayforpay.types';
|
|
7
|
+
import type { Logger } from '../logger/types';
|
|
8
|
+
import type { IPaymentService, PaymentEntity } from './types';
|
|
9
|
+
import type { IInvoiceService, InvoiceEntity } from '../invoice/types';
|
|
10
|
+
import type { ISubscriptionService, SubscriptionEntity } from '../subscription/types';
|
|
11
|
+
import type { ISubscriptionPlanService, SubscriptionPlanEntity } from '../subscriptionPlan/types';
|
|
12
|
+
import type { ITokenPackService, TokenPackEntity } from '../tokenPack/types';
|
|
13
|
+
import type { GracePeriodConfig } from '../../config';
|
|
14
|
+
import type { TokenPackPlan } from '../token/types';
|
|
15
|
+
import { createSubscriptionOrderReference, createTokenPackOrderReference } from './utils';
|
|
16
|
+
|
|
17
|
+
const MERCHANT_SECRET_KEY = 'test-secret-key';
|
|
18
|
+
const MERCHANT_ACCOUNT = 'test_merchant';
|
|
19
|
+
|
|
20
|
+
// Mock logger
|
|
21
|
+
const createMockLogger = (): Logger => ({
|
|
22
|
+
info: mock.fn(),
|
|
23
|
+
warning: mock.fn(),
|
|
24
|
+
error: mock.fn(),
|
|
25
|
+
debug: mock.fn(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Mock response object
|
|
29
|
+
const createMockResponse = () => {
|
|
30
|
+
let statusCode: number | undefined;
|
|
31
|
+
let jsonData: unknown;
|
|
32
|
+
|
|
33
|
+
const res = {
|
|
34
|
+
status: mock.fn((code: number) => {
|
|
35
|
+
statusCode = code;
|
|
36
|
+
return res;
|
|
37
|
+
}),
|
|
38
|
+
json: mock.fn((data: unknown) => {
|
|
39
|
+
jsonData = data;
|
|
40
|
+
}),
|
|
41
|
+
getStatusCode: () => statusCode,
|
|
42
|
+
getJsonData: () => jsonData,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return res;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Calculate signature for callback data
|
|
49
|
+
const calculateSignature = (data: Partial<WayForPayCallbackData>, secretKey: string): string => {
|
|
50
|
+
const signatureData = [
|
|
51
|
+
data.merchantAccount,
|
|
52
|
+
data.orderReference,
|
|
53
|
+
data.amount,
|
|
54
|
+
data.currency,
|
|
55
|
+
data.authCode,
|
|
56
|
+
data.cardPan,
|
|
57
|
+
data.transactionStatus,
|
|
58
|
+
data.reasonCode,
|
|
59
|
+
];
|
|
60
|
+
const signatureString = signatureData.join(';');
|
|
61
|
+
return crypto.createHmac('md5', secretKey).update(signatureString).digest('hex');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Mock callback data factory
|
|
65
|
+
const createMockCallbackData = (
|
|
66
|
+
overrides: Partial<WayForPayCallbackData> = {},
|
|
67
|
+
secretKey: string = MERCHANT_SECRET_KEY,
|
|
68
|
+
): WayForPayCallbackData => {
|
|
69
|
+
const baseData: Omit<WayForPayCallbackData, 'merchantSignature'> = {
|
|
70
|
+
merchantAccount: MERCHANT_ACCOUNT,
|
|
71
|
+
orderReference: 'miia_telegram_bot_user123_sub_plan-monthly_v2_20240101120000',
|
|
72
|
+
amount: 100,
|
|
73
|
+
currency: 'UAH',
|
|
74
|
+
authCode: 'auth-123',
|
|
75
|
+
cardPan: '4111****1111',
|
|
76
|
+
transactionStatus: 'Approved',
|
|
77
|
+
reasonCode: '1100',
|
|
78
|
+
email: 'test@example.com',
|
|
79
|
+
phone: '+380123456789',
|
|
80
|
+
createdDate: Math.floor(Date.now() / 1000),
|
|
81
|
+
processingDate: Math.floor(Date.now() / 1000),
|
|
82
|
+
cardType: 'VISA',
|
|
83
|
+
issuerBankCountry: 'UA',
|
|
84
|
+
issuerBankName: 'Test Bank',
|
|
85
|
+
recToken: 'rec-token-123',
|
|
86
|
+
reason: 'Ok',
|
|
87
|
+
fee: 2.5,
|
|
88
|
+
paymentSystem: 'VISA',
|
|
89
|
+
...overrides,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const merchantSignature =
|
|
93
|
+
overrides.merchantSignature !== undefined
|
|
94
|
+
? overrides.merchantSignature
|
|
95
|
+
: calculateSignature(baseData as Partial<WayForPayCallbackData>, secretKey);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...baseData,
|
|
99
|
+
merchantSignature,
|
|
100
|
+
} as WayForPayCallbackData;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Mock WayForPay service
|
|
104
|
+
const createMockWayForPayService = (): IWayForPayService => ({
|
|
105
|
+
generateSubscriptionPaymentUrl: mock.fn(async () => null),
|
|
106
|
+
generateOneTimePaymentUrl: mock.fn(async () => null),
|
|
107
|
+
verifyCallbackSignature: mock.fn((data: WayForPayCallbackData) => {
|
|
108
|
+
const expectedSignature = calculateSignature(data, MERCHANT_SECRET_KEY);
|
|
109
|
+
const isValid = data.merchantSignature?.toLowerCase() === expectedSignature.toLowerCase();
|
|
110
|
+
return {
|
|
111
|
+
isValid,
|
|
112
|
+
error: isValid ? undefined : data.merchantSignature ? undefined : 'Missing merchant signature in callback data',
|
|
113
|
+
};
|
|
114
|
+
}),
|
|
115
|
+
buildWebhookResponse: mock.fn((orderReference: string, status: 'accept' | 'decline'): WayForPayWebhookResponse => {
|
|
116
|
+
const time = Math.floor(Date.now() / 1000);
|
|
117
|
+
const signatureData = [orderReference, status, time];
|
|
118
|
+
const signatureString = signatureData.join(';');
|
|
119
|
+
const signature = crypto.createHmac('md5', MERCHANT_SECRET_KEY).update(signatureString).digest('hex');
|
|
120
|
+
return { orderReference, status, time, signature };
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Mock payment service
|
|
125
|
+
const createMockPaymentService = (payment: PaymentEntity | null = null): IPaymentService => ({
|
|
126
|
+
create: mock.fn(async () => ({ id: 'payment-1', status: 'pending' } as PaymentEntity)),
|
|
127
|
+
getByOrderReference: mock.fn(async () => payment),
|
|
128
|
+
getByUser: mock.fn(async () => (payment ? [payment] : [])),
|
|
129
|
+
updateStatus: mock.fn(async () => {}),
|
|
130
|
+
changeStatus: mock.fn(async () => {}),
|
|
131
|
+
getExpiredPendingPayments: mock.fn(async () => []),
|
|
132
|
+
createPaymentIntent: mock.fn(async () => ({ id: 'payment-1' } as PaymentEntity)),
|
|
133
|
+
createTokenPackPaymentIntent: mock.fn(async () => ({ id: 'payment-1' } as PaymentEntity)),
|
|
134
|
+
createUpgradePaymentIntent: mock.fn(async () => ({ id: 'payment-1' } as PaymentEntity)),
|
|
135
|
+
validateSignature: mock.fn(() => ({ isValid: true })),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Mock invoice service
|
|
139
|
+
const createMockInvoiceService = (existingInvoice: InvoiceEntity | null = null): IInvoiceService => ({
|
|
140
|
+
create: mock.fn(async (data) => ({
|
|
141
|
+
id: 'invoice-1',
|
|
142
|
+
...data,
|
|
143
|
+
} as InvoiceEntity)),
|
|
144
|
+
getByOrderReference: mock.fn(async () => existingInvoice),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Mock subscription service
|
|
148
|
+
const createMockSubscriptionService = (
|
|
149
|
+
existingSubscription: SubscriptionEntity | null = null,
|
|
150
|
+
): ISubscriptionService => ({
|
|
151
|
+
activateSubscription: mock.fn(async () => {}),
|
|
152
|
+
getByUser: mock.fn(async () => existingSubscription),
|
|
153
|
+
create: mock.fn(async () => ({ id: 'sub-1', status: 'active' } as SubscriptionEntity)),
|
|
154
|
+
updateFieldsByUserId: mock.fn(async () => {}),
|
|
155
|
+
getExpiredActiveSubscriptions: mock.fn(async () => []),
|
|
156
|
+
updateFieldsById: mock.fn(async () => {}),
|
|
157
|
+
getOrCreateSubscriptionPaymentUrl: mock.fn(async () => 'https://payment.url'),
|
|
158
|
+
getSubscriptionsForDeactivationCheck: mock.fn(async () => []),
|
|
159
|
+
renewSubscription: mock.fn(async () => {}),
|
|
160
|
+
cancelSubscription: mock.fn(async () => {}),
|
|
161
|
+
deactivateSubscription: mock.fn(async () => {}),
|
|
162
|
+
getExpiredSubscriptions: mock.fn(async () => []),
|
|
163
|
+
getFailedRenewals: mock.fn(async () => []),
|
|
164
|
+
deactivate: mock.fn(async () => {}),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Mock subscription plan service
|
|
168
|
+
const createMockSubscriptionPlanService = (
|
|
169
|
+
plan: SubscriptionPlanEntity | null = {
|
|
170
|
+
id: 'plan-monthly',
|
|
171
|
+
titleCode: 'plans.monthly.title',
|
|
172
|
+
descriptionCode: 'plans.monthly.description',
|
|
173
|
+
amount: 100,
|
|
174
|
+
currency: 'UAH',
|
|
175
|
+
features: { messages: 1000, images: 100, voice: 50 },
|
|
176
|
+
regularMode: 'monthly',
|
|
177
|
+
count: 1,
|
|
178
|
+
credits: 1000,
|
|
179
|
+
isActive: true,
|
|
180
|
+
createdAt: new Date().toISOString(),
|
|
181
|
+
updatedAt: new Date().toISOString(),
|
|
182
|
+
},
|
|
183
|
+
): ISubscriptionPlanService => ({
|
|
184
|
+
getById: mock.fn(async () => plan),
|
|
185
|
+
getAll: mock.fn(async () => (plan ? [plan] : [])),
|
|
186
|
+
create: mock.fn(async () => plan as SubscriptionPlanEntity),
|
|
187
|
+
update: mock.fn(async () => plan as SubscriptionPlanEntity),
|
|
188
|
+
deactivate: mock.fn(async () => {}),
|
|
189
|
+
resetCache: mock.fn(() => {}),
|
|
190
|
+
seedDefaults: mock.fn(async () => {}),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Mock token pack service
|
|
194
|
+
const createMockTokenPackService = (): ITokenPackService => ({
|
|
195
|
+
create: mock.fn(async () => ({
|
|
196
|
+
id: 'token-pack-1',
|
|
197
|
+
userId: 'user123',
|
|
198
|
+
platform: 'telegram',
|
|
199
|
+
packId: 'pack-small',
|
|
200
|
+
tokens: 10000,
|
|
201
|
+
tokensRemaining: 10000,
|
|
202
|
+
status: 'active',
|
|
203
|
+
purchasedAt: new Date().toISOString(),
|
|
204
|
+
provider: 'wayforpay',
|
|
205
|
+
} as TokenPackEntity)),
|
|
206
|
+
getByUser: mock.fn(async () => []),
|
|
207
|
+
getById: mock.fn(async () => null),
|
|
208
|
+
updateFieldsById: mock.fn(async () => {}),
|
|
209
|
+
deductTokens: mock.fn(async () => {}),
|
|
210
|
+
getExpiredActiveTokenPacks: mock.fn(async () => []),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Grace period config
|
|
214
|
+
const createMockGracePeriodConfig = (enabled: boolean = true): GracePeriodConfig => ({
|
|
215
|
+
durationMs: 3 * 24 * 60 * 60 * 1000, // 3 days
|
|
216
|
+
enabled,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Minimal mock service for validation-only tests (avoids Firebase initialization)
|
|
220
|
+
const createMinimalMockWebhookService = () => ({
|
|
221
|
+
processPaymentWebhook: mock.fn(async () => ({
|
|
222
|
+
success: true,
|
|
223
|
+
response: { orderReference: 'test', status: 'accept' as const, time: Date.now(), signature: 'sig' },
|
|
224
|
+
})),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('WebhookHandler Integration Tests', () => {
|
|
228
|
+
let logger: Logger;
|
|
229
|
+
|
|
230
|
+
beforeEach(() => {
|
|
231
|
+
logger = createMockLogger();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('Purchase Transaction Webhook - HTTP Handler', () => {
|
|
235
|
+
describe('Request Validation', () => {
|
|
236
|
+
it('should return 400 for non-object request body', async () => {
|
|
237
|
+
const handler = new WebhookHandler({ logger, webhookService: createMinimalMockWebhookService() });
|
|
238
|
+
const res = createMockResponse();
|
|
239
|
+
|
|
240
|
+
await handler.handleRequest({ body: null }, res);
|
|
241
|
+
|
|
242
|
+
assert.strictEqual(res.getStatusCode(), 400);
|
|
243
|
+
const jsonData = res.getJsonData() as { error: string };
|
|
244
|
+
assert.strictEqual(jsonData.error, 'Request body must be a JSON object');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should return 400 for missing required fields', async () => {
|
|
248
|
+
const handler = new WebhookHandler({ logger, webhookService: createMinimalMockWebhookService() });
|
|
249
|
+
const res = createMockResponse();
|
|
250
|
+
|
|
251
|
+
await handler.handleRequest({ body: { merchantAccount: 'test' } }, res);
|
|
252
|
+
|
|
253
|
+
assert.strictEqual(res.getStatusCode(), 400);
|
|
254
|
+
const jsonData = res.getJsonData() as { error: string };
|
|
255
|
+
assert.ok(jsonData.error.includes('Missing required fields'));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should return 400 for invalid transaction status', async () => {
|
|
259
|
+
const handler = new WebhookHandler({ logger, webhookService: createMinimalMockWebhookService() });
|
|
260
|
+
const res = createMockResponse();
|
|
261
|
+
|
|
262
|
+
await handler.handleRequest({
|
|
263
|
+
body: {
|
|
264
|
+
merchantAccount: 'test',
|
|
265
|
+
orderReference: 'order-123',
|
|
266
|
+
merchantSignature: 'sig',
|
|
267
|
+
amount: 100,
|
|
268
|
+
currency: 'UAH',
|
|
269
|
+
transactionStatus: 'InvalidStatus',
|
|
270
|
+
},
|
|
271
|
+
}, res);
|
|
272
|
+
|
|
273
|
+
assert.strictEqual(res.getStatusCode(), 400);
|
|
274
|
+
const jsonData = res.getJsonData() as { error: string };
|
|
275
|
+
assert.ok(jsonData.error.includes('Invalid transaction status'));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should accept valid request body with all required fields', async () => {
|
|
279
|
+
const wayForPayService = createMockWayForPayService();
|
|
280
|
+
const webhookService = new WebhookHandlerService({
|
|
281
|
+
logger,
|
|
282
|
+
wayForPayService,
|
|
283
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
284
|
+
invoiceService: createMockInvoiceService(),
|
|
285
|
+
subscriptionService: createMockSubscriptionService(),
|
|
286
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
287
|
+
tokenPackService: createMockTokenPackService(),
|
|
288
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const handler = new WebhookHandler({ logger, webhookService });
|
|
292
|
+
const res = createMockResponse();
|
|
293
|
+
const callbackData = createMockCallbackData();
|
|
294
|
+
|
|
295
|
+
await handler.handleRequest({ body: callbackData }, res);
|
|
296
|
+
|
|
297
|
+
assert.strictEqual(res.getStatusCode(), 200);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('Signature Validation', () => {
|
|
302
|
+
it('should decline webhook with invalid signature', async () => {
|
|
303
|
+
const wayForPayService = createMockWayForPayService();
|
|
304
|
+
const webhookService = new WebhookHandlerService({
|
|
305
|
+
logger,
|
|
306
|
+
wayForPayService,
|
|
307
|
+
paymentService: createMockPaymentService(),
|
|
308
|
+
invoiceService: createMockInvoiceService(),
|
|
309
|
+
subscriptionService: createMockSubscriptionService(),
|
|
310
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
311
|
+
tokenPackService: createMockTokenPackService(),
|
|
312
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const handler = new WebhookHandler({ logger, webhookService });
|
|
316
|
+
const res = createMockResponse();
|
|
317
|
+
const callbackData = createMockCallbackData({ merchantSignature: 'invalid-signature' });
|
|
318
|
+
|
|
319
|
+
await handler.handleRequest({ body: callbackData }, res);
|
|
320
|
+
|
|
321
|
+
assert.strictEqual(res.getStatusCode(), 200);
|
|
322
|
+
const jsonData = res.getJsonData() as WayForPayWebhookResponse;
|
|
323
|
+
assert.strictEqual(jsonData.status, 'decline');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should decline webhook with missing signature', async () => {
|
|
327
|
+
const wayForPayService = createMockWayForPayService();
|
|
328
|
+
const webhookService = new WebhookHandlerService({
|
|
329
|
+
logger,
|
|
330
|
+
wayForPayService,
|
|
331
|
+
paymentService: createMockPaymentService(),
|
|
332
|
+
invoiceService: createMockInvoiceService(),
|
|
333
|
+
subscriptionService: createMockSubscriptionService(),
|
|
334
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
335
|
+
tokenPackService: createMockTokenPackService(),
|
|
336
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const handler = new WebhookHandler({ logger, webhookService });
|
|
340
|
+
const res = createMockResponse();
|
|
341
|
+
const callbackData = createMockCallbackData({ merchantSignature: '' });
|
|
342
|
+
|
|
343
|
+
await handler.handleRequest({ body: callbackData }, res);
|
|
344
|
+
|
|
345
|
+
assert.strictEqual(res.getStatusCode(), 200);
|
|
346
|
+
const jsonData = res.getJsonData() as WayForPayWebhookResponse;
|
|
347
|
+
assert.strictEqual(jsonData.status, 'decline');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should accept webhook with valid signature (case-insensitive)', async () => {
|
|
351
|
+
const wayForPayService = createMockWayForPayService();
|
|
352
|
+
const webhookService = new WebhookHandlerService({
|
|
353
|
+
logger,
|
|
354
|
+
wayForPayService,
|
|
355
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
356
|
+
invoiceService: createMockInvoiceService(),
|
|
357
|
+
subscriptionService: createMockSubscriptionService(),
|
|
358
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
359
|
+
tokenPackService: createMockTokenPackService(),
|
|
360
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const handler = new WebhookHandler({ logger, webhookService });
|
|
364
|
+
const res = createMockResponse();
|
|
365
|
+
const callbackData = createMockCallbackData();
|
|
366
|
+
// Make signature uppercase
|
|
367
|
+
callbackData.merchantSignature = callbackData.merchantSignature.toUpperCase();
|
|
368
|
+
|
|
369
|
+
await handler.handleRequest({ body: callbackData }, res);
|
|
370
|
+
|
|
371
|
+
assert.strictEqual(res.getStatusCode(), 200);
|
|
372
|
+
const jsonData = res.getJsonData() as WayForPayWebhookResponse;
|
|
373
|
+
assert.strictEqual(jsonData.status, 'accept');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('Purchase Transaction Webhook - Service Processing', () => {
|
|
379
|
+
describe('Approved Subscription Payment', () => {
|
|
380
|
+
it('should create invoice for approved subscription payment', async () => {
|
|
381
|
+
const invoiceService = createMockInvoiceService();
|
|
382
|
+
const webhookService = new WebhookHandlerService({
|
|
383
|
+
logger,
|
|
384
|
+
wayForPayService: createMockWayForPayService(),
|
|
385
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
386
|
+
invoiceService,
|
|
387
|
+
subscriptionService: createMockSubscriptionService(),
|
|
388
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
389
|
+
tokenPackService: createMockTokenPackService(),
|
|
390
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const orderReference = createSubscriptionOrderReference({
|
|
394
|
+
userId: 'user123',
|
|
395
|
+
platform: 'telegram',
|
|
396
|
+
planId: 'plan-monthly',
|
|
397
|
+
utcDate: '20240101120000',
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const callbackData = createMockCallbackData({
|
|
401
|
+
orderReference,
|
|
402
|
+
transactionStatus: 'Approved',
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
406
|
+
|
|
407
|
+
assert.strictEqual(result.success, true);
|
|
408
|
+
assert.strictEqual(result.response.status, 'accept');
|
|
409
|
+
assert.strictEqual((invoiceService.create as any).mock.calls.length, 1);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should activate new subscription when none exists', async () => {
|
|
413
|
+
const subscriptionService = createMockSubscriptionService(null);
|
|
414
|
+
const webhookService = new WebhookHandlerService({
|
|
415
|
+
logger,
|
|
416
|
+
wayForPayService: createMockWayForPayService(),
|
|
417
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
418
|
+
invoiceService: createMockInvoiceService(),
|
|
419
|
+
subscriptionService,
|
|
420
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
421
|
+
tokenPackService: createMockTokenPackService(),
|
|
422
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const orderReference = createSubscriptionOrderReference({
|
|
426
|
+
userId: 'user123',
|
|
427
|
+
platform: 'telegram',
|
|
428
|
+
planId: 'plan-monthly',
|
|
429
|
+
utcDate: '20240101120000',
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const callbackData = createMockCallbackData({
|
|
433
|
+
orderReference,
|
|
434
|
+
transactionStatus: 'Approved',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
438
|
+
|
|
439
|
+
assert.strictEqual(result.success, true);
|
|
440
|
+
assert.strictEqual((subscriptionService.activateSubscription as any).mock.calls.length, 1);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should renew existing subscription', async () => {
|
|
444
|
+
const existingSubscription: SubscriptionEntity = {
|
|
445
|
+
id: 'sub-1',
|
|
446
|
+
userId: 'user123',
|
|
447
|
+
platform: 'telegram',
|
|
448
|
+
planId: 'plan-monthly',
|
|
449
|
+
status: 'active',
|
|
450
|
+
startedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
451
|
+
expiresAt: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
452
|
+
provider: 'wayforpay',
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const subscriptionService = createMockSubscriptionService(existingSubscription);
|
|
456
|
+
const webhookService = new WebhookHandlerService({
|
|
457
|
+
logger,
|
|
458
|
+
wayForPayService: createMockWayForPayService(),
|
|
459
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
460
|
+
invoiceService: createMockInvoiceService(),
|
|
461
|
+
subscriptionService,
|
|
462
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
463
|
+
tokenPackService: createMockTokenPackService(),
|
|
464
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const orderReference = createSubscriptionOrderReference({
|
|
468
|
+
userId: 'user123',
|
|
469
|
+
platform: 'telegram',
|
|
470
|
+
planId: 'plan-monthly',
|
|
471
|
+
utcDate: '20240101120000',
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const callbackData = createMockCallbackData({
|
|
475
|
+
orderReference,
|
|
476
|
+
transactionStatus: 'Approved',
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
480
|
+
|
|
481
|
+
assert.strictEqual(result.success, true);
|
|
482
|
+
assert.strictEqual((subscriptionService.renewSubscription as any).mock.calls.length, 1);
|
|
483
|
+
assert.strictEqual((subscriptionService.activateSubscription as any).mock.calls.length, 0);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should update payment status to completed', async () => {
|
|
487
|
+
const paymentService = createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity);
|
|
488
|
+
const webhookService = new WebhookHandlerService({
|
|
489
|
+
logger,
|
|
490
|
+
wayForPayService: createMockWayForPayService(),
|
|
491
|
+
paymentService,
|
|
492
|
+
invoiceService: createMockInvoiceService(),
|
|
493
|
+
subscriptionService: createMockSubscriptionService(),
|
|
494
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
495
|
+
tokenPackService: createMockTokenPackService(),
|
|
496
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const orderReference = createSubscriptionOrderReference({
|
|
500
|
+
userId: 'user123',
|
|
501
|
+
platform: 'telegram',
|
|
502
|
+
planId: 'plan-monthly',
|
|
503
|
+
utcDate: '20240101120000',
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const callbackData = createMockCallbackData({
|
|
507
|
+
orderReference,
|
|
508
|
+
transactionStatus: 'Approved',
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await webhookService.processPaymentWebhook(callbackData);
|
|
512
|
+
|
|
513
|
+
assert.strictEqual((paymentService.changeStatus as any).mock.calls.length, 1);
|
|
514
|
+
const changeStatusCall = (paymentService.changeStatus as any).mock.calls[0];
|
|
515
|
+
assert.strictEqual(changeStatusCall.arguments[0].status, 'completed');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should initialize token balance based on subscription plan', async () => {
|
|
519
|
+
const tokenPackService = createMockTokenPackService();
|
|
520
|
+
const webhookService = new WebhookHandlerService({
|
|
521
|
+
logger,
|
|
522
|
+
wayForPayService: createMockWayForPayService(),
|
|
523
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
524
|
+
invoiceService: createMockInvoiceService(),
|
|
525
|
+
subscriptionService: createMockSubscriptionService(),
|
|
526
|
+
subscriptionPlanService: createMockSubscriptionPlanService({
|
|
527
|
+
id: 'plan-monthly',
|
|
528
|
+
titleCode: 'plans.monthly.title',
|
|
529
|
+
descriptionCode: 'plans.monthly.description',
|
|
530
|
+
amount: 100,
|
|
531
|
+
currency: 'UAH',
|
|
532
|
+
features: { messages: 1000, images: 100, voice: 50 },
|
|
533
|
+
regularMode: 'monthly',
|
|
534
|
+
count: 1,
|
|
535
|
+
credits: 5000,
|
|
536
|
+
isActive: true,
|
|
537
|
+
createdAt: new Date().toISOString(),
|
|
538
|
+
updatedAt: new Date().toISOString(),
|
|
539
|
+
}),
|
|
540
|
+
tokenPackService,
|
|
541
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const orderReference = createSubscriptionOrderReference({
|
|
545
|
+
userId: 'user123',
|
|
546
|
+
platform: 'telegram',
|
|
547
|
+
planId: 'plan-monthly',
|
|
548
|
+
utcDate: '20240101120000',
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
const callbackData = createMockCallbackData({
|
|
552
|
+
orderReference,
|
|
553
|
+
transactionStatus: 'Approved',
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await webhookService.processPaymentWebhook(callbackData);
|
|
557
|
+
|
|
558
|
+
assert.strictEqual((tokenPackService.create as any).mock.calls.length, 1);
|
|
559
|
+
const createCall = (tokenPackService.create as any).mock.calls[0];
|
|
560
|
+
assert.strictEqual(createCall.arguments[0].tokens, 5000);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should handle duplicate webhook (idempotency)', async () => {
|
|
564
|
+
const existingInvoice: InvoiceEntity = {
|
|
565
|
+
id: 'invoice-1',
|
|
566
|
+
merchantAccount: MERCHANT_ACCOUNT,
|
|
567
|
+
orderReference: 'miia_telegram_bot_user123_sub_plan-monthly_v2_20240101120000',
|
|
568
|
+
merchantSignature: 'sig',
|
|
569
|
+
reasonCode: 1100,
|
|
570
|
+
reason: 'Ok',
|
|
571
|
+
createdDate: new Date().toISOString(),
|
|
572
|
+
processingDate: new Date().toISOString(),
|
|
573
|
+
currency: 'UAH',
|
|
574
|
+
amount: 100,
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const invoiceService = createMockInvoiceService(existingInvoice);
|
|
578
|
+
const subscriptionService = createMockSubscriptionService();
|
|
579
|
+
const webhookService = new WebhookHandlerService({
|
|
580
|
+
logger,
|
|
581
|
+
wayForPayService: createMockWayForPayService(),
|
|
582
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
583
|
+
invoiceService,
|
|
584
|
+
subscriptionService,
|
|
585
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
586
|
+
tokenPackService: createMockTokenPackService(),
|
|
587
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const callbackData = createMockCallbackData({
|
|
591
|
+
orderReference: existingInvoice.orderReference,
|
|
592
|
+
transactionStatus: 'Approved',
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
596
|
+
|
|
597
|
+
assert.strictEqual(result.success, true);
|
|
598
|
+
// Invoice should not be created again
|
|
599
|
+
assert.strictEqual((invoiceService.create as any).mock.calls.length, 0);
|
|
600
|
+
// Subscription should not be activated again
|
|
601
|
+
assert.strictEqual((subscriptionService.activateSubscription as any).mock.calls.length, 0);
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
describe('Approved Token Pack Payment', () => {
|
|
606
|
+
it('should create invoice for approved token pack payment', async () => {
|
|
607
|
+
const invoiceService = createMockInvoiceService();
|
|
608
|
+
const tokenPackPlans: TokenPackPlan[] = [{
|
|
609
|
+
id: 'pack-small',
|
|
610
|
+
titleCode: 'packs.small.title',
|
|
611
|
+
descriptionCode: 'packs.small.description',
|
|
612
|
+
tokens: 10000,
|
|
613
|
+
amount: 49,
|
|
614
|
+
currency: 'UAH',
|
|
615
|
+
isActive: true,
|
|
616
|
+
}];
|
|
617
|
+
|
|
618
|
+
const webhookService = new WebhookHandlerService({
|
|
619
|
+
logger,
|
|
620
|
+
wayForPayService: createMockWayForPayService(),
|
|
621
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
622
|
+
invoiceService,
|
|
623
|
+
subscriptionService: createMockSubscriptionService(),
|
|
624
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
625
|
+
tokenPackService: createMockTokenPackService(),
|
|
626
|
+
tokenPackPlans,
|
|
627
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const orderReference = createTokenPackOrderReference({
|
|
631
|
+
userId: 'user123',
|
|
632
|
+
platform: 'telegram',
|
|
633
|
+
packId: 'pack-small',
|
|
634
|
+
utcDate: '20240101120000',
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const callbackData = createMockCallbackData({
|
|
638
|
+
orderReference,
|
|
639
|
+
transactionStatus: 'Approved',
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
643
|
+
|
|
644
|
+
assert.strictEqual(result.success, true);
|
|
645
|
+
assert.strictEqual(result.response.status, 'accept');
|
|
646
|
+
assert.strictEqual((invoiceService.create as any).mock.calls.length, 1);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('should create token pack for user', async () => {
|
|
650
|
+
const tokenPackService = createMockTokenPackService();
|
|
651
|
+
const tokenPackPlans: TokenPackPlan[] = [{
|
|
652
|
+
id: 'pack-small',
|
|
653
|
+
titleCode: 'packs.small.title',
|
|
654
|
+
descriptionCode: 'packs.small.description',
|
|
655
|
+
tokens: 10000,
|
|
656
|
+
amount: 49,
|
|
657
|
+
currency: 'UAH',
|
|
658
|
+
isActive: true,
|
|
659
|
+
}];
|
|
660
|
+
|
|
661
|
+
const webhookService = new WebhookHandlerService({
|
|
662
|
+
logger,
|
|
663
|
+
wayForPayService: createMockWayForPayService(),
|
|
664
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
665
|
+
invoiceService: createMockInvoiceService(),
|
|
666
|
+
subscriptionService: createMockSubscriptionService(),
|
|
667
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
668
|
+
tokenPackService,
|
|
669
|
+
tokenPackPlans,
|
|
670
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const orderReference = createTokenPackOrderReference({
|
|
674
|
+
userId: 'user123',
|
|
675
|
+
platform: 'telegram',
|
|
676
|
+
packId: 'pack-small',
|
|
677
|
+
utcDate: '20240101120000',
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const callbackData = createMockCallbackData({
|
|
681
|
+
orderReference,
|
|
682
|
+
transactionStatus: 'Approved',
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await webhookService.processPaymentWebhook(callbackData);
|
|
686
|
+
|
|
687
|
+
assert.strictEqual((tokenPackService.create as any).mock.calls.length, 1);
|
|
688
|
+
const createCall = (tokenPackService.create as any).mock.calls[0];
|
|
689
|
+
assert.strictEqual(createCall.arguments[0].tokens, 10000);
|
|
690
|
+
assert.strictEqual(createCall.arguments[0].userId, 'user123');
|
|
691
|
+
assert.strictEqual(createCall.arguments[0].packId, 'pack-small');
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
describe('Declined/Failed Payment', () => {
|
|
696
|
+
it('should accept webhook for declined transaction without processing', async () => {
|
|
697
|
+
const invoiceService = createMockInvoiceService();
|
|
698
|
+
const subscriptionService = createMockSubscriptionService();
|
|
699
|
+
const webhookService = new WebhookHandlerService({
|
|
700
|
+
logger,
|
|
701
|
+
wayForPayService: createMockWayForPayService(),
|
|
702
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
703
|
+
invoiceService,
|
|
704
|
+
subscriptionService,
|
|
705
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
706
|
+
tokenPackService: createMockTokenPackService(),
|
|
707
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const orderReference = createSubscriptionOrderReference({
|
|
711
|
+
userId: 'user123',
|
|
712
|
+
platform: 'telegram',
|
|
713
|
+
planId: 'plan-monthly',
|
|
714
|
+
utcDate: '20240101120000',
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
const callbackData = createMockCallbackData({
|
|
718
|
+
orderReference,
|
|
719
|
+
transactionStatus: 'Declined',
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
723
|
+
|
|
724
|
+
assert.strictEqual(result.success, true);
|
|
725
|
+
assert.strictEqual(result.response.status, 'accept');
|
|
726
|
+
// No invoice should be created for declined transaction
|
|
727
|
+
assert.strictEqual((invoiceService.create as any).mock.calls.length, 0);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('should update payment status to failed for declined transaction', async () => {
|
|
731
|
+
const paymentService = createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity);
|
|
732
|
+
const webhookService = new WebhookHandlerService({
|
|
733
|
+
logger,
|
|
734
|
+
wayForPayService: createMockWayForPayService(),
|
|
735
|
+
paymentService,
|
|
736
|
+
invoiceService: createMockInvoiceService(),
|
|
737
|
+
subscriptionService: createMockSubscriptionService(),
|
|
738
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
739
|
+
tokenPackService: createMockTokenPackService(),
|
|
740
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const orderReference = createSubscriptionOrderReference({
|
|
744
|
+
userId: 'user123',
|
|
745
|
+
platform: 'telegram',
|
|
746
|
+
planId: 'plan-monthly',
|
|
747
|
+
utcDate: '20240101120000',
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const callbackData = createMockCallbackData({
|
|
751
|
+
orderReference,
|
|
752
|
+
transactionStatus: 'Declined',
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
await webhookService.processPaymentWebhook(callbackData);
|
|
756
|
+
|
|
757
|
+
assert.strictEqual((paymentService.changeStatus as any).mock.calls.length, 1);
|
|
758
|
+
const changeStatusCall = (paymentService.changeStatus as any).mock.calls[0];
|
|
759
|
+
assert.strictEqual(changeStatusCall.arguments[0].status, 'failed');
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should accept webhook for expired transaction', async () => {
|
|
763
|
+
const webhookService = new WebhookHandlerService({
|
|
764
|
+
logger,
|
|
765
|
+
wayForPayService: createMockWayForPayService(),
|
|
766
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
767
|
+
invoiceService: createMockInvoiceService(),
|
|
768
|
+
subscriptionService: createMockSubscriptionService(),
|
|
769
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
770
|
+
tokenPackService: createMockTokenPackService(),
|
|
771
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
const orderReference = createSubscriptionOrderReference({
|
|
775
|
+
userId: 'user123',
|
|
776
|
+
platform: 'telegram',
|
|
777
|
+
planId: 'plan-monthly',
|
|
778
|
+
utcDate: '20240101120000',
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const callbackData = createMockCallbackData({
|
|
782
|
+
orderReference,
|
|
783
|
+
transactionStatus: 'Expired',
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
787
|
+
|
|
788
|
+
assert.strictEqual(result.success, true);
|
|
789
|
+
assert.strictEqual(result.response.status, 'accept');
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('should handle failed recurring renewal - deactivate subscription past grace period', async () => {
|
|
793
|
+
const pastExpiration = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000); // 5 days ago
|
|
794
|
+
const existingSubscription: SubscriptionEntity = {
|
|
795
|
+
id: 'sub-1',
|
|
796
|
+
userId: 'user123',
|
|
797
|
+
platform: 'telegram',
|
|
798
|
+
planId: 'plan-monthly',
|
|
799
|
+
status: 'active',
|
|
800
|
+
startedAt: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
|
801
|
+
expiresAt: pastExpiration.toISOString(),
|
|
802
|
+
provider: 'wayforpay',
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
const subscriptionService = createMockSubscriptionService(existingSubscription);
|
|
806
|
+
const webhookService = new WebhookHandlerService({
|
|
807
|
+
logger,
|
|
808
|
+
wayForPayService: createMockWayForPayService(),
|
|
809
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
810
|
+
invoiceService: createMockInvoiceService(),
|
|
811
|
+
subscriptionService,
|
|
812
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
813
|
+
tokenPackService: createMockTokenPackService(),
|
|
814
|
+
gracePeriodConfig: createMockGracePeriodConfig(true),
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const orderReference = createSubscriptionOrderReference({
|
|
818
|
+
userId: 'user123',
|
|
819
|
+
platform: 'telegram',
|
|
820
|
+
planId: 'plan-monthly',
|
|
821
|
+
utcDate: '20240101120000',
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const callbackData = createMockCallbackData({
|
|
825
|
+
orderReference,
|
|
826
|
+
transactionStatus: 'Declined',
|
|
827
|
+
regularOn: '1', // Indicates recurring payment
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
await webhookService.processPaymentWebhook(callbackData);
|
|
831
|
+
|
|
832
|
+
// Should deactivate since past grace period
|
|
833
|
+
assert.strictEqual((subscriptionService.deactivateSubscription as any).mock.calls.length, 1);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it('should handle failed recurring renewal - cancel subscription within grace period', async () => {
|
|
837
|
+
const recentExpiration = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000); // 1 day ago (within 3-day grace)
|
|
838
|
+
const existingSubscription: SubscriptionEntity = {
|
|
839
|
+
id: 'sub-1',
|
|
840
|
+
userId: 'user123',
|
|
841
|
+
platform: 'telegram',
|
|
842
|
+
planId: 'plan-monthly',
|
|
843
|
+
status: 'active',
|
|
844
|
+
startedAt: new Date(Date.now() - 32 * 24 * 60 * 60 * 1000).toISOString(),
|
|
845
|
+
expiresAt: recentExpiration.toISOString(),
|
|
846
|
+
provider: 'wayforpay',
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const subscriptionService = createMockSubscriptionService(existingSubscription);
|
|
850
|
+
const webhookService = new WebhookHandlerService({
|
|
851
|
+
logger,
|
|
852
|
+
wayForPayService: createMockWayForPayService(),
|
|
853
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
854
|
+
invoiceService: createMockInvoiceService(),
|
|
855
|
+
subscriptionService,
|
|
856
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
857
|
+
tokenPackService: createMockTokenPackService(),
|
|
858
|
+
gracePeriodConfig: createMockGracePeriodConfig(true),
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const orderReference = createSubscriptionOrderReference({
|
|
862
|
+
userId: 'user123',
|
|
863
|
+
platform: 'telegram',
|
|
864
|
+
planId: 'plan-monthly',
|
|
865
|
+
utcDate: '20240101120000',
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
const callbackData = createMockCallbackData({
|
|
869
|
+
orderReference,
|
|
870
|
+
transactionStatus: 'Declined',
|
|
871
|
+
regularOn: '1',
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
await webhookService.processPaymentWebhook(callbackData);
|
|
875
|
+
|
|
876
|
+
// Should cancel (not deactivate) since within grace period
|
|
877
|
+
assert.strictEqual((subscriptionService.cancelSubscription as any).mock.calls.length, 1);
|
|
878
|
+
assert.strictEqual((subscriptionService.deactivateSubscription as any).mock.calls.length, 0);
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
describe('Error Handling', () => {
|
|
883
|
+
it('should return decline response on processing error', async () => {
|
|
884
|
+
const subscriptionPlanService = createMockSubscriptionPlanService(null);
|
|
885
|
+
const webhookService = new WebhookHandlerService({
|
|
886
|
+
logger,
|
|
887
|
+
wayForPayService: createMockWayForPayService(),
|
|
888
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
889
|
+
invoiceService: createMockInvoiceService(),
|
|
890
|
+
subscriptionService: createMockSubscriptionService(),
|
|
891
|
+
subscriptionPlanService,
|
|
892
|
+
tokenPackService: createMockTokenPackService(),
|
|
893
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
const orderReference = createSubscriptionOrderReference({
|
|
897
|
+
userId: 'user123',
|
|
898
|
+
platform: 'telegram',
|
|
899
|
+
planId: 'non-existent-plan',
|
|
900
|
+
utcDate: '20240101120000',
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
const callbackData = createMockCallbackData({
|
|
904
|
+
orderReference,
|
|
905
|
+
transactionStatus: 'Approved',
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
909
|
+
|
|
910
|
+
assert.strictEqual(result.success, false);
|
|
911
|
+
assert.strictEqual(result.response.status, 'decline');
|
|
912
|
+
assert.ok(result.error);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it('should handle invalid order reference format', async () => {
|
|
916
|
+
const webhookService = new WebhookHandlerService({
|
|
917
|
+
logger,
|
|
918
|
+
wayForPayService: createMockWayForPayService(),
|
|
919
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
920
|
+
invoiceService: createMockInvoiceService(),
|
|
921
|
+
subscriptionService: createMockSubscriptionService(),
|
|
922
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
923
|
+
tokenPackService: createMockTokenPackService(),
|
|
924
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const callbackData = createMockCallbackData({
|
|
928
|
+
orderReference: 'invalid-format',
|
|
929
|
+
transactionStatus: 'Approved',
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
const result = await webhookService.processPaymentWebhook(callbackData);
|
|
933
|
+
|
|
934
|
+
// Unknown payment type should just accept
|
|
935
|
+
assert.strictEqual(result.success, true);
|
|
936
|
+
assert.strictEqual(result.response.status, 'accept');
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
describe('Direct Callback Processing', () => {
|
|
942
|
+
it('should process callback directly via processCallback method', async () => {
|
|
943
|
+
const wayForPayService = createMockWayForPayService();
|
|
944
|
+
const webhookService = new WebhookHandlerService({
|
|
945
|
+
logger,
|
|
946
|
+
wayForPayService,
|
|
947
|
+
paymentService: createMockPaymentService({ id: 'pay-1', status: 'pending' } as PaymentEntity),
|
|
948
|
+
invoiceService: createMockInvoiceService(),
|
|
949
|
+
subscriptionService: createMockSubscriptionService(),
|
|
950
|
+
subscriptionPlanService: createMockSubscriptionPlanService(),
|
|
951
|
+
tokenPackService: createMockTokenPackService(),
|
|
952
|
+
gracePeriodConfig: createMockGracePeriodConfig(),
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
const handler = new WebhookHandler({ logger, webhookService });
|
|
956
|
+
|
|
957
|
+
const orderReference = createSubscriptionOrderReference({
|
|
958
|
+
userId: 'user123',
|
|
959
|
+
platform: 'telegram',
|
|
960
|
+
planId: 'plan-monthly',
|
|
961
|
+
utcDate: '20240101120000',
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
const callbackData = createMockCallbackData({
|
|
965
|
+
orderReference,
|
|
966
|
+
transactionStatus: 'Approved',
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const result = await handler.processCallback(callbackData);
|
|
970
|
+
|
|
971
|
+
assert.strictEqual(result.success, true);
|
|
972
|
+
assert.strictEqual(result.response.status, 'accept');
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
});
|