@misterhomer1992/miit-bot-payment 1.1.6 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/config/ConfigurationManager.d.ts +64 -0
  2. package/dist/config/ConfigurationManager.d.ts.map +1 -0
  3. package/dist/config/ConfigurationManager.js +144 -0
  4. package/dist/config/ConfigurationManager.js.map +1 -0
  5. package/dist/config/defaults.d.ts +18 -0
  6. package/dist/config/defaults.d.ts.map +1 -0
  7. package/dist/config/defaults.js +26 -0
  8. package/dist/config/defaults.js.map +1 -0
  9. package/dist/config/environment.d.ts +38 -0
  10. package/dist/config/environment.d.ts.map +1 -0
  11. package/dist/config/environment.js +91 -0
  12. package/dist/config/environment.js.map +1 -0
  13. package/dist/config/index.d.ts +5 -0
  14. package/dist/config/index.d.ts.map +1 -0
  15. package/dist/config/index.js +18 -0
  16. package/dist/config/index.js.map +1 -0
  17. package/dist/config/types.d.ts +53 -0
  18. package/dist/config/types.d.ts.map +1 -0
  19. package/dist/config/types.js +3 -0
  20. package/dist/config/types.js.map +1 -0
  21. package/dist/index.d.ts +21 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +23 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/modules/cache/InMemoryCache.d.ts +17 -0
  26. package/dist/modules/cache/InMemoryCache.d.ts.map +1 -0
  27. package/dist/modules/cache/InMemoryCache.js +77 -0
  28. package/dist/modules/cache/InMemoryCache.js.map +1 -0
  29. package/dist/modules/cache/index.d.ts +3 -0
  30. package/dist/modules/cache/index.d.ts.map +1 -0
  31. package/dist/modules/cache/index.js +19 -0
  32. package/dist/modules/cache/index.js.map +1 -0
  33. package/dist/modules/cache/types.d.ts +52 -0
  34. package/dist/modules/cache/types.d.ts.map +1 -0
  35. package/dist/modules/cache/types.js +3 -0
  36. package/dist/modules/cache/types.js.map +1 -0
  37. package/dist/modules/errors/index.d.ts +2 -0
  38. package/dist/modules/errors/index.d.ts.map +1 -0
  39. package/dist/modules/errors/index.js +19 -0
  40. package/dist/modules/errors/index.js.map +1 -0
  41. package/dist/modules/errors/types.d.ts +112 -0
  42. package/dist/modules/errors/types.d.ts.map +1 -0
  43. package/dist/modules/errors/types.js +174 -0
  44. package/dist/modules/errors/types.js.map +1 -0
  45. package/dist/modules/payments/api.d.ts +63 -1
  46. package/dist/modules/payments/api.d.ts.map +1 -1
  47. package/dist/modules/payments/api.js +103 -1
  48. package/dist/modules/payments/api.js.map +1 -1
  49. package/dist/modules/payments/const.d.ts.map +1 -1
  50. package/dist/modules/payments/const.js +1 -0
  51. package/dist/modules/payments/const.js.map +1 -1
  52. package/dist/modules/payments/index.d.ts +8 -0
  53. package/dist/modules/payments/index.d.ts.map +1 -1
  54. package/dist/modules/payments/index.js +8 -0
  55. package/dist/modules/payments/index.js.map +1 -1
  56. package/dist/modules/payments/service.d.ts +42 -2
  57. package/dist/modules/payments/service.d.ts.map +1 -1
  58. package/dist/modules/payments/service.js +132 -3
  59. package/dist/modules/payments/service.js.map +1 -1
  60. package/dist/modules/payments/subscription-check-webhook.handler.d.ts +85 -0
  61. package/dist/modules/payments/subscription-check-webhook.handler.d.ts.map +1 -0
  62. package/dist/modules/payments/subscription-check-webhook.handler.js +155 -0
  63. package/dist/modules/payments/subscription-check-webhook.handler.js.map +1 -0
  64. package/dist/modules/payments/subscription-check-webhook.service.d.ts +59 -0
  65. package/dist/modules/payments/subscription-check-webhook.service.d.ts.map +1 -0
  66. package/dist/modules/payments/subscription-check-webhook.service.js +330 -0
  67. package/dist/modules/payments/subscription-check-webhook.service.js.map +1 -0
  68. package/dist/modules/payments/subscription-check-webhook.types.d.ts +25 -0
  69. package/dist/modules/payments/subscription-check-webhook.types.d.ts.map +1 -0
  70. package/dist/modules/payments/subscription-check-webhook.types.js +3 -0
  71. package/dist/modules/payments/subscription-check-webhook.types.js.map +1 -0
  72. package/dist/modules/payments/types.d.ts +69 -2
  73. package/dist/modules/payments/types.d.ts.map +1 -1
  74. package/dist/modules/payments/utils.d.ts +151 -5
  75. package/dist/modules/payments/utils.d.ts.map +1 -1
  76. package/dist/modules/payments/utils.js +253 -9
  77. package/dist/modules/payments/utils.js.map +1 -1
  78. package/dist/modules/payments/wayforpay.service.d.ts +39 -0
  79. package/dist/modules/payments/wayforpay.service.d.ts.map +1 -0
  80. package/dist/modules/payments/wayforpay.service.js +217 -0
  81. package/dist/modules/payments/wayforpay.service.js.map +1 -0
  82. package/dist/modules/payments/wayforpay.types.d.ts +115 -0
  83. package/dist/modules/payments/wayforpay.types.d.ts.map +1 -0
  84. package/dist/modules/payments/wayforpay.types.js +3 -0
  85. package/dist/modules/payments/wayforpay.types.js.map +1 -0
  86. package/dist/modules/payments/webhook.handler.d.ts +98 -0
  87. package/dist/modules/payments/webhook.handler.d.ts.map +1 -0
  88. package/dist/modules/payments/webhook.handler.js +153 -0
  89. package/dist/modules/payments/webhook.handler.js.map +1 -0
  90. package/dist/modules/payments/webhook.service.d.ts +99 -0
  91. package/dist/modules/payments/webhook.service.d.ts.map +1 -0
  92. package/dist/modules/payments/webhook.service.js +672 -0
  93. package/dist/modules/payments/webhook.service.js.map +1 -0
  94. package/dist/modules/payments/webhook.types.d.ts +35 -0
  95. package/dist/modules/payments/webhook.types.d.ts.map +1 -0
  96. package/dist/modules/payments/webhook.types.js +3 -0
  97. package/dist/modules/payments/webhook.types.js.map +1 -0
  98. package/dist/modules/subscription/change.service.d.ts +80 -0
  99. package/dist/modules/subscription/change.service.d.ts.map +1 -0
  100. package/dist/modules/subscription/change.service.js +226 -0
  101. package/dist/modules/subscription/change.service.js.map +1 -0
  102. package/dist/modules/subscription/index.d.ts +2 -0
  103. package/dist/modules/subscription/index.d.ts.map +1 -1
  104. package/dist/modules/subscription/index.js +2 -0
  105. package/dist/modules/subscription/index.js.map +1 -1
  106. package/dist/modules/subscription/service.d.ts +8 -1
  107. package/dist/modules/subscription/service.d.ts.map +1 -1
  108. package/dist/modules/subscription/service.js +59 -2
  109. package/dist/modules/subscription/service.js.map +1 -1
  110. package/dist/modules/subscription/status-check.handler.d.ts +117 -0
  111. package/dist/modules/subscription/status-check.handler.d.ts.map +1 -0
  112. package/dist/modules/subscription/status-check.handler.js +164 -0
  113. package/dist/modules/subscription/status-check.handler.js.map +1 -0
  114. package/dist/modules/subscription/types.d.ts +37 -1
  115. package/dist/modules/subscription/types.d.ts.map +1 -1
  116. package/dist/modules/subscriptionPlan/const.d.ts +5 -0
  117. package/dist/modules/subscriptionPlan/const.d.ts.map +1 -0
  118. package/dist/modules/subscriptionPlan/const.js +106 -0
  119. package/dist/modules/subscriptionPlan/const.js.map +1 -0
  120. package/dist/modules/subscriptionPlan/index.d.ts +5 -0
  121. package/dist/modules/subscriptionPlan/index.d.ts.map +1 -0
  122. package/dist/modules/subscriptionPlan/index.js +21 -0
  123. package/dist/modules/subscriptionPlan/index.js.map +1 -0
  124. package/dist/modules/subscriptionPlan/repository.d.ts +22 -0
  125. package/dist/modules/subscriptionPlan/repository.d.ts.map +1 -0
  126. package/dist/modules/subscriptionPlan/repository.js +95 -0
  127. package/dist/modules/subscriptionPlan/repository.js.map +1 -0
  128. package/dist/modules/subscriptionPlan/service.d.ts +21 -0
  129. package/dist/modules/subscriptionPlan/service.d.ts.map +1 -0
  130. package/dist/modules/subscriptionPlan/service.js +128 -0
  131. package/dist/modules/subscriptionPlan/service.js.map +1 -0
  132. package/dist/modules/subscriptionPlan/types.d.ts +40 -0
  133. package/dist/modules/subscriptionPlan/types.d.ts.map +1 -0
  134. package/dist/modules/subscriptionPlan/types.js +3 -0
  135. package/dist/modules/subscriptionPlan/types.js.map +1 -0
  136. package/dist/modules/token/const.d.ts +7 -0
  137. package/dist/modules/token/const.d.ts.map +1 -0
  138. package/dist/modules/token/const.js +66 -0
  139. package/dist/modules/token/const.js.map +1 -0
  140. package/dist/modules/token/index.d.ts +4 -0
  141. package/dist/modules/token/index.d.ts.map +1 -0
  142. package/dist/modules/token/index.js +20 -0
  143. package/dist/modules/token/index.js.map +1 -0
  144. package/dist/modules/token/service.d.ts +46 -0
  145. package/dist/modules/token/service.d.ts.map +1 -0
  146. package/dist/modules/token/service.js +249 -0
  147. package/dist/modules/token/service.js.map +1 -0
  148. package/dist/modules/token/types.d.ts +109 -0
  149. package/dist/modules/token/types.d.ts.map +1 -0
  150. package/dist/modules/token/types.js +3 -0
  151. package/dist/modules/token/types.js.map +1 -0
  152. package/dist/modules/tokenPack/const.d.ts +4 -0
  153. package/dist/modules/tokenPack/const.d.ts.map +1 -0
  154. package/dist/modules/tokenPack/const.js +10 -0
  155. package/dist/modules/tokenPack/const.js.map +1 -0
  156. package/dist/modules/tokenPack/index.d.ts +5 -0
  157. package/dist/modules/tokenPack/index.d.ts.map +1 -0
  158. package/dist/modules/tokenPack/index.js +21 -0
  159. package/dist/modules/tokenPack/index.js.map +1 -0
  160. package/dist/modules/tokenPack/repository.d.ts +32 -0
  161. package/dist/modules/tokenPack/repository.d.ts.map +1 -0
  162. package/dist/modules/tokenPack/repository.js +103 -0
  163. package/dist/modules/tokenPack/repository.js.map +1 -0
  164. package/dist/modules/tokenPack/service.d.ts +28 -0
  165. package/dist/modules/tokenPack/service.d.ts.map +1 -0
  166. package/dist/modules/tokenPack/service.js +106 -0
  167. package/dist/modules/tokenPack/service.js.map +1 -0
  168. package/dist/modules/tokenPack/types.d.ts +124 -0
  169. package/dist/modules/tokenPack/types.d.ts.map +1 -0
  170. package/dist/modules/tokenPack/types.js +3 -0
  171. package/dist/modules/tokenPack/types.js.map +1 -0
  172. package/package.json +9 -5
  173. package/src/config/ConfigurationManager.ts +159 -0
  174. package/src/config/defaults.ts +27 -0
  175. package/src/config/environment.ts +94 -0
  176. package/src/config/index.ts +22 -0
  177. package/src/config/types.ts +56 -0
  178. package/src/index.ts +29 -0
  179. package/src/modules/cache/InMemoryCache.ts +98 -0
  180. package/src/modules/cache/index.ts +2 -0
  181. package/src/modules/cache/types.ts +60 -0
  182. package/src/modules/cancellableAPI/utils.ts +60 -0
  183. package/src/modules/errors/index.ts +16 -0
  184. package/src/modules/errors/types.ts +201 -0
  185. package/src/modules/invoice/const.ts +7 -0
  186. package/src/modules/invoice/index.ts +4 -0
  187. package/src/modules/invoice/repository.ts +52 -0
  188. package/src/modules/invoice/service.ts +44 -0
  189. package/src/modules/invoice/types.ts +47 -0
  190. package/src/modules/logger/types.ts +8 -0
  191. package/src/modules/network/utils.ts +24 -0
  192. package/src/modules/payments/api.ts +289 -0
  193. package/src/modules/payments/const.ts +11 -0
  194. package/src/modules/payments/index.ts +14 -0
  195. package/src/modules/payments/repository.ts +125 -0
  196. package/src/modules/payments/service.test.ts +400 -0
  197. package/src/modules/payments/service.ts +365 -0
  198. package/src/modules/payments/subscription-check-webhook.handler.integration.test.ts +935 -0
  199. package/src/modules/payments/subscription-check-webhook.handler.ts +211 -0
  200. package/src/modules/payments/subscription-check-webhook.service.ts +398 -0
  201. package/src/modules/payments/subscription-check-webhook.types.ts +29 -0
  202. package/src/modules/payments/types.ts +193 -0
  203. package/src/modules/payments/utils.ts +428 -0
  204. package/src/modules/payments/wayforpay.service.test.ts +375 -0
  205. package/src/modules/payments/wayforpay.service.ts +284 -0
  206. package/src/modules/payments/wayforpay.types.ts +138 -0
  207. package/src/modules/payments/webhook.handler.integration.test.ts +975 -0
  208. package/src/modules/payments/webhook.handler.ts +219 -0
  209. package/src/modules/payments/webhook.service.ts +812 -0
  210. package/src/modules/payments/webhook.types.ts +38 -0
  211. package/src/modules/subscription/change.service.ts +317 -0
  212. package/src/modules/subscription/const.ts +9 -0
  213. package/src/modules/subscription/index.ts +5 -0
  214. package/src/modules/subscription/repository.ts +277 -0
  215. package/src/modules/subscription/service.test.ts +665 -0
  216. package/src/modules/subscription/service.ts +328 -0
  217. package/src/modules/subscription/status-check.handler.ts +254 -0
  218. package/src/modules/subscription/types.ts +267 -0
  219. package/src/modules/subscription/utils.ts +5 -0
  220. package/src/modules/subscriptionPlan/const.ts +106 -0
  221. package/src/modules/subscriptionPlan/index.ts +4 -0
  222. package/src/modules/subscriptionPlan/repository.ts +129 -0
  223. package/src/modules/subscriptionPlan/service.test.ts +401 -0
  224. package/src/modules/subscriptionPlan/service.ts +148 -0
  225. package/src/modules/subscriptionPlan/types.ts +67 -0
  226. package/src/modules/token/const.ts +64 -0
  227. package/src/modules/token/index.ts +3 -0
  228. package/src/modules/token/service.test.ts +499 -0
  229. package/src/modules/token/service.ts +297 -0
  230. package/src/modules/token/types.ts +124 -0
  231. package/src/modules/tokenPack/const.ts +9 -0
  232. package/src/modules/tokenPack/index.ts +4 -0
  233. package/src/modules/tokenPack/repository.ts +144 -0
  234. package/src/modules/tokenPack/service.ts +119 -0
  235. package/src/modules/tokenPack/types.ts +131 -0
  236. package/src/modules/user/index.ts +3 -0
  237. package/src/modules/user/types.ts +143 -0
  238. package/src/modules/user/userRepository.ts +64 -0
  239. package/src/modules/user/userService.ts +68 -0
  240. package/src/types/extend-express.d.ts +16 -0
  241. package/src/types/function.ts +5 -0
  242. package/src/types/utilities.ts +22 -0
  243. package/src/utils.ts +53 -0
  244. package/tsconfig.json +29 -0
  245. package/dist/modules/subscription/subscriptionPlan.d.ts +0 -4
  246. package/dist/modules/subscription/subscriptionPlan.d.ts.map +0 -1
  247. package/dist/modules/subscription/subscriptionPlan.js +0 -67
  248. 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
+ });