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