@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.
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 +58 -1
  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,812 @@
1
+ import moment from 'moment';
2
+ import type { ResponseTransactionStatus } from '@misterhomer1992/wayforpay-api';
3
+ import { Logger } from '../logger/types';
4
+
5
+ const APPROVED_STATUS: ResponseTransactionStatus = 'Approved';
6
+ const DECLINED_STATUS: ResponseTransactionStatus = 'Declined';
7
+ const EXPIRED_STATUS: ResponseTransactionStatus = 'Expired';
8
+ import { WayForPayService } from './wayforpay.service';
9
+ import { PaymentService } from './service';
10
+ import { InvoiceService } from '../invoice/service';
11
+ import { SubscriptionService } from '../subscription/service';
12
+ import { SubscriptionPlanService } from '../subscriptionPlan/service';
13
+ import { TokenPackService } from '../tokenPack/service';
14
+ import {
15
+ parseOrderReference,
16
+ isSubscriptionOrderReference,
17
+ isTokenPackOrderReference,
18
+ isUpgradeOrderReference,
19
+ getOrderReferenceUserId,
20
+ getOrderReferenceItemId,
21
+ parseUpgradeOrderReference,
22
+ } from './utils';
23
+ import type { WayForPayCallbackData, IWayForPayService } from './wayforpay.types';
24
+ import type { IPaymentService } from './types';
25
+ import type { IInvoiceService } from '../invoice/types';
26
+ import type { ISubscriptionService, SubscriptionEntity } from '../subscription/types';
27
+ import type { ISubscriptionPlanService } from '../subscriptionPlan/types';
28
+ import type { ITokenPackService } from '../tokenPack/types';
29
+ import type { WebhookProcessingResult, IWebhookHandlerService } from './webhook.types';
30
+ import { DEFAULT_TOKEN_PACK_PLANS } from '../token/const';
31
+ import type { TokenPackPlan } from '../token/types';
32
+ import { ConfigurationManager, GracePeriodConfig, DEFAULT_GRACE_PERIOD_CONFIG } from '../../config';
33
+
34
+ /**
35
+ * WebhookHandlerService processes payment confirmation webhooks from WayForPay.
36
+ * It handles signature verification, payment status updates, subscription activation,
37
+ * and user token balance initialization.
38
+ */
39
+ export class WebhookHandlerService implements IWebhookHandlerService {
40
+ private readonly logger: Logger;
41
+ private readonly wayForPayService: IWayForPayService;
42
+ private readonly paymentService: IPaymentService;
43
+ private readonly invoiceService: IInvoiceService;
44
+ private readonly subscriptionService: ISubscriptionService;
45
+ private readonly subscriptionPlanService: ISubscriptionPlanService;
46
+ private readonly tokenPackService: ITokenPackService;
47
+ private readonly tokenPackPlans: TokenPackPlan[];
48
+ private readonly gracePeriodConfig: GracePeriodConfig;
49
+
50
+ constructor({
51
+ logger,
52
+ wayForPayService,
53
+ paymentService,
54
+ invoiceService,
55
+ subscriptionService,
56
+ subscriptionPlanService,
57
+ tokenPackService,
58
+ tokenPackPlans,
59
+ gracePeriodConfig,
60
+ }: {
61
+ logger: Logger;
62
+ wayForPayService?: IWayForPayService;
63
+ paymentService?: IPaymentService;
64
+ invoiceService?: IInvoiceService;
65
+ subscriptionService?: ISubscriptionService;
66
+ subscriptionPlanService?: ISubscriptionPlanService;
67
+ tokenPackService?: ITokenPackService;
68
+ tokenPackPlans?: TokenPackPlan[];
69
+ gracePeriodConfig?: GracePeriodConfig;
70
+ }) {
71
+ this.logger = logger;
72
+ this.wayForPayService = wayForPayService || new WayForPayService({ logger });
73
+ this.paymentService = paymentService || new PaymentService({ logger });
74
+ this.invoiceService = invoiceService || new InvoiceService({ logger });
75
+ this.subscriptionService = subscriptionService || new SubscriptionService({ logger });
76
+ this.subscriptionPlanService = subscriptionPlanService || new SubscriptionPlanService({ logger });
77
+ this.tokenPackService = tokenPackService || new TokenPackService({ logger });
78
+ this.tokenPackPlans = tokenPackPlans || DEFAULT_TOKEN_PACK_PLANS;
79
+ this.gracePeriodConfig = gracePeriodConfig || this.loadGracePeriodConfig();
80
+ }
81
+
82
+ private loadGracePeriodConfig(): GracePeriodConfig {
83
+ try {
84
+ const configManager = ConfigurationManager.getInstance();
85
+ return configManager.getGracePeriodConfig();
86
+ } catch {
87
+ return DEFAULT_GRACE_PERIOD_CONFIG;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Processes a payment confirmation webhook from WayForPay.
93
+ * This method handles the complete webhook flow:
94
+ * 1. Verifies the signature to validate request authenticity
95
+ * 2. Creates an invoice record
96
+ * 3. Updates payment status
97
+ * 4. Activates subscription or renews existing one
98
+ * 5. Initializes user token balance based on plan
99
+ *
100
+ * @param callbackData - The callback data from WayForPay
101
+ * @returns Processing result with response to send back to WayForPay
102
+ */
103
+ public async processPaymentWebhook(callbackData: WayForPayCallbackData): Promise<WebhookProcessingResult> {
104
+ const { orderReference, transactionStatus } = callbackData;
105
+
106
+ this.logger.info({
107
+ message: 'Processing WayForPay webhook',
108
+ payload: {
109
+ orderReference,
110
+ transactionStatus,
111
+ },
112
+ });
113
+
114
+ try {
115
+ // Step 1: Verify signature to validate request authenticity
116
+ const signatureResult = this.wayForPayService.verifyCallbackSignature(callbackData);
117
+
118
+ if (!signatureResult.isValid) {
119
+ this.logger.warning({
120
+ message: 'Invalid webhook signature',
121
+ payload: {
122
+ orderReference,
123
+ error: signatureResult.error,
124
+ },
125
+ });
126
+
127
+ return {
128
+ success: false,
129
+ response: this.wayForPayService.buildWebhookResponse(orderReference, 'decline'),
130
+ error: signatureResult.error || 'Invalid signature',
131
+ };
132
+ }
133
+
134
+ // Step 2: Check if this is an approved transaction
135
+ if (transactionStatus !== APPROVED_STATUS) {
136
+ this.logger.info({
137
+ message: 'Webhook received for non-approved transaction',
138
+ payload: {
139
+ orderReference,
140
+ transactionStatus,
141
+ },
142
+ });
143
+
144
+ // Handle declined or other statuses
145
+ if (transactionStatus === DECLINED_STATUS || transactionStatus === EXPIRED_STATUS) {
146
+ await this.handleFailedPayment(callbackData);
147
+ }
148
+
149
+ return {
150
+ success: true,
151
+ response: this.wayForPayService.buildWebhookResponse(orderReference, 'accept'),
152
+ };
153
+ }
154
+
155
+ // Step 3: Process based on payment type
156
+ if (isUpgradeOrderReference(orderReference)) {
157
+ // Process upgrade payment
158
+ await this.processApprovedUpgradePayment(callbackData);
159
+ } else if (isSubscriptionOrderReference(orderReference)) {
160
+ // Process subscription payment
161
+ await this.processApprovedSubscriptionPayment(callbackData);
162
+ } else if (isTokenPackOrderReference(orderReference)) {
163
+ // Process token pack payment
164
+ await this.processApprovedTokenPackPayment(callbackData);
165
+ } else {
166
+ this.logger.info({
167
+ message: 'Webhook received for unknown payment type',
168
+ payload: { orderReference },
169
+ });
170
+ }
171
+
172
+ return {
173
+ success: true,
174
+ response: this.wayForPayService.buildWebhookResponse(orderReference, 'accept'),
175
+ };
176
+ } catch (error) {
177
+ this.logger.error({
178
+ message: 'Error processing webhook',
179
+ payload: {
180
+ orderReference,
181
+ error: JSON.stringify(error),
182
+ },
183
+ });
184
+
185
+ return {
186
+ success: false,
187
+ response: this.wayForPayService.buildWebhookResponse(orderReference, 'decline'),
188
+ error: error instanceof Error ? error.message : 'Unknown error',
189
+ };
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Processes an approved subscription payment.
195
+ * Creates invoice, updates payment status, activates subscription,
196
+ * and initializes token balance.
197
+ */
198
+ private async processApprovedSubscriptionPayment(callbackData: WayForPayCallbackData): Promise<void> {
199
+ const { orderReference, merchantAccount, merchantSignature, reasonCode, reason, amount, currency } =
200
+ callbackData;
201
+
202
+ // Parse order reference to extract user and plan info
203
+ const parsedReference = parseOrderReference(orderReference);
204
+ if (!parsedReference) {
205
+ throw new Error(`Invalid order reference format: ${orderReference}`);
206
+ }
207
+
208
+ const userId = getOrderReferenceUserId(orderReference);
209
+ const planId = getOrderReferenceItemId(orderReference);
210
+ const { platform } = parsedReference;
211
+
212
+ if (!userId || !planId) {
213
+ throw new Error(`Could not extract userId or planId from order reference: ${orderReference}`);
214
+ }
215
+
216
+ // Check for duplicate webhook (idempotency)
217
+ const existingInvoice = await this.invoiceService.getByOrderReference(orderReference);
218
+ if (existingInvoice) {
219
+ this.logger.info({
220
+ message: 'Duplicate webhook received, already processed',
221
+ payload: { orderReference },
222
+ });
223
+ return;
224
+ }
225
+
226
+ // Create invoice record
227
+ // reasonCode from WayForPay is a string like '1100', convert to number for storage
228
+ const reasonCodeNumber = typeof reasonCode === 'string' ? parseInt(reasonCode, 10) : Number(reasonCode);
229
+ const invoice = await this.invoiceService.create({
230
+ merchantAccount,
231
+ orderReference,
232
+ merchantSignature,
233
+ reasonCode: reasonCodeNumber,
234
+ reason,
235
+ createdDate: moment.unix(callbackData.createdDate).toISOString(),
236
+ processingDate: moment.unix(callbackData.processingDate).toISOString(),
237
+ currency,
238
+ amount,
239
+ });
240
+
241
+ this.logger.info({
242
+ message: 'Invoice created',
243
+ payload: { invoiceId: invoice.id, orderReference },
244
+ });
245
+
246
+ // Update payment status to completed
247
+ const payment = await this.paymentService.getByOrderReference(orderReference);
248
+ if (payment && payment.id) {
249
+ await this.paymentService.changeStatus({
250
+ id: payment.id,
251
+ status: 'completed',
252
+ });
253
+
254
+ this.logger.info({
255
+ message: 'Payment status updated to completed',
256
+ payload: { paymentId: payment.id, orderReference },
257
+ });
258
+ }
259
+
260
+ // Get subscription plan to calculate expiration and tokens
261
+ const subscriptionPlan = await this.subscriptionPlanService.getById(planId);
262
+ if (!subscriptionPlan) {
263
+ throw new Error(`Subscription plan not found: ${planId}`);
264
+ }
265
+
266
+ // Calculate subscription expiration date
267
+ const startedAt = moment.utc().toISOString();
268
+ const expiresAt = this.calculateExpirationDate(subscriptionPlan.regularMode, subscriptionPlan.count);
269
+
270
+ // Check for existing active subscription
271
+ const existingSubscription = await this.subscriptionService.getByUser({
272
+ userId,
273
+ platform,
274
+ status: 'active',
275
+ });
276
+
277
+ if (existingSubscription) {
278
+ // Renew existing subscription
279
+ await this.renewSubscription(existingSubscription, expiresAt);
280
+ } else {
281
+ // Activate new subscription
282
+ await this.activateNewSubscription({
283
+ userId,
284
+ platform,
285
+ planId,
286
+ startedAt,
287
+ expiresAt,
288
+ });
289
+ }
290
+
291
+ // Initialize user token balance based on plan credits
292
+ await this.initializeTokenBalance({
293
+ userId,
294
+ platform,
295
+ planId,
296
+ tokens: subscriptionPlan.credits,
297
+ });
298
+
299
+ this.logger.info({
300
+ message: 'Subscription activated successfully',
301
+ payload: {
302
+ userId,
303
+ platform,
304
+ planId,
305
+ expiresAt,
306
+ tokens: subscriptionPlan.credits,
307
+ },
308
+ });
309
+ }
310
+
311
+ /**
312
+ * Processes an approved token pack payment.
313
+ * Creates invoice, updates payment status, and adds tokens to user balance.
314
+ */
315
+ private async processApprovedTokenPackPayment(callbackData: WayForPayCallbackData): Promise<void> {
316
+ const { orderReference, merchantAccount, merchantSignature, reasonCode, reason, amount, currency } =
317
+ callbackData;
318
+
319
+ // Parse order reference to extract user and pack info
320
+ const parsedReference = parseOrderReference(orderReference);
321
+ if (!parsedReference) {
322
+ throw new Error(`Invalid order reference format: ${orderReference}`);
323
+ }
324
+
325
+ const userId = getOrderReferenceUserId(orderReference);
326
+ const packId = getOrderReferenceItemId(orderReference);
327
+ const { platform } = parsedReference;
328
+
329
+ if (!userId || !packId) {
330
+ throw new Error(`Could not extract userId or packId from order reference: ${orderReference}`);
331
+ }
332
+
333
+ // Check for duplicate webhook (idempotency)
334
+ const existingInvoice = await this.invoiceService.getByOrderReference(orderReference);
335
+ if (existingInvoice) {
336
+ this.logger.info({
337
+ message: 'Duplicate webhook received for token pack, already processed',
338
+ payload: { orderReference },
339
+ });
340
+ return;
341
+ }
342
+
343
+ // Create invoice record
344
+ const reasonCodeNumber = typeof reasonCode === 'string' ? parseInt(reasonCode, 10) : Number(reasonCode);
345
+ const invoice = await this.invoiceService.create({
346
+ merchantAccount,
347
+ orderReference,
348
+ merchantSignature,
349
+ reasonCode: reasonCodeNumber,
350
+ reason,
351
+ createdDate: moment.unix(callbackData.createdDate).toISOString(),
352
+ processingDate: moment.unix(callbackData.processingDate).toISOString(),
353
+ currency,
354
+ amount,
355
+ });
356
+
357
+ this.logger.info({
358
+ message: 'Invoice created for token pack',
359
+ payload: { invoiceId: invoice.id, orderReference },
360
+ });
361
+
362
+ // Update payment status to completed
363
+ const payment = await this.paymentService.getByOrderReference(orderReference);
364
+ if (payment && payment.id) {
365
+ await this.paymentService.changeStatus({
366
+ id: payment.id,
367
+ status: 'completed',
368
+ });
369
+
370
+ this.logger.info({
371
+ message: 'Token pack payment status updated to completed',
372
+ payload: { paymentId: payment.id, orderReference },
373
+ });
374
+ }
375
+
376
+ // Get token pack plan to determine token count
377
+ const tokenPackPlan = this.tokenPackPlans.find((p) => p.id === packId);
378
+ if (!tokenPackPlan) {
379
+ throw new Error(`Token pack plan not found: ${packId}`);
380
+ }
381
+
382
+ // Create token pack for user
383
+ await this.tokenPackService.create({
384
+ userId,
385
+ platform,
386
+ packId,
387
+ tokens: tokenPackPlan.tokens,
388
+ purchasedAt: moment.utc().toISOString(),
389
+ provider: 'wayforpay',
390
+ });
391
+
392
+ this.logger.info({
393
+ message: 'Token pack activated successfully',
394
+ payload: {
395
+ userId,
396
+ platform,
397
+ packId,
398
+ tokens: tokenPackPlan.tokens,
399
+ },
400
+ });
401
+ }
402
+
403
+ /**
404
+ * Processes an approved subscription upgrade payment.
405
+ * Updates subscription's planId, resets expiresAt, and adds new plan's tokens to existing balance.
406
+ */
407
+ private async processApprovedUpgradePayment(callbackData: WayForPayCallbackData): Promise<void> {
408
+ const { orderReference, merchantAccount, merchantSignature, reasonCode, reason, amount, currency } =
409
+ callbackData;
410
+
411
+ // Parse upgrade order reference to extract both plan IDs
412
+ const upgradeParsed = parseUpgradeOrderReference(orderReference);
413
+ if (!upgradeParsed) {
414
+ throw new Error(`Invalid upgrade order reference format: ${orderReference}`);
415
+ }
416
+
417
+ const { userId, platform, currentPlanId, newPlanId } = upgradeParsed;
418
+
419
+ if (!userId || !currentPlanId || !newPlanId) {
420
+ throw new Error(`Could not extract userId or plan IDs from upgrade order reference: ${orderReference}`);
421
+ }
422
+
423
+ // Check for duplicate webhook (idempotency)
424
+ const existingInvoice = await this.invoiceService.getByOrderReference(orderReference);
425
+ if (existingInvoice) {
426
+ this.logger.info({
427
+ message: 'Duplicate webhook received for upgrade, already processed',
428
+ payload: { orderReference },
429
+ });
430
+ return;
431
+ }
432
+
433
+ // Create invoice record
434
+ const reasonCodeNumber = typeof reasonCode === 'string' ? parseInt(reasonCode, 10) : Number(reasonCode);
435
+ const invoice = await this.invoiceService.create({
436
+ merchantAccount,
437
+ orderReference,
438
+ merchantSignature,
439
+ reasonCode: reasonCodeNumber,
440
+ reason,
441
+ createdDate: moment.unix(callbackData.createdDate).toISOString(),
442
+ processingDate: moment.unix(callbackData.processingDate).toISOString(),
443
+ currency,
444
+ amount,
445
+ });
446
+
447
+ this.logger.info({
448
+ message: 'Invoice created for upgrade',
449
+ payload: { invoiceId: invoice.id, orderReference },
450
+ });
451
+
452
+ // Update payment status to completed
453
+ const payment = await this.paymentService.getByOrderReference(orderReference);
454
+ if (payment && payment.id) {
455
+ await this.paymentService.changeStatus({
456
+ id: payment.id,
457
+ status: 'completed',
458
+ });
459
+
460
+ this.logger.info({
461
+ message: 'Upgrade payment status updated to completed',
462
+ payload: { paymentId: payment.id, orderReference },
463
+ });
464
+ }
465
+
466
+ // Get the new subscription plan
467
+ const newPlan = await this.subscriptionPlanService.getById(newPlanId);
468
+ if (!newPlan) {
469
+ throw new Error(`New subscription plan not found: ${newPlanId}`);
470
+ }
471
+
472
+ // Get existing active subscription
473
+ const existingSubscription = await this.subscriptionService.getByUser({
474
+ userId,
475
+ platform,
476
+ status: 'active',
477
+ });
478
+
479
+ if (!existingSubscription) {
480
+ this.logger.warning({
481
+ message: 'No active subscription found for upgrade, treating as new subscription',
482
+ payload: { userId, platform, orderReference },
483
+ });
484
+
485
+ // Fall back to creating a new subscription
486
+ const startedAt = moment.utc().toISOString();
487
+ const expiresAt = this.calculateExpirationDate(newPlan.regularMode, newPlan.count);
488
+
489
+ await this.activateNewSubscription({
490
+ userId,
491
+ platform,
492
+ planId: newPlanId,
493
+ startedAt,
494
+ expiresAt,
495
+ });
496
+
497
+ // Initialize token balance with new plan's credits
498
+ await this.initializeTokenBalance({
499
+ userId,
500
+ platform,
501
+ planId: newPlanId,
502
+ tokens: newPlan.credits,
503
+ });
504
+
505
+ return;
506
+ }
507
+
508
+ // Calculate new expiration date (full new period from now)
509
+ const startedAt = moment.utc().toISOString();
510
+ const expiresAt = this.calculateExpirationDate(newPlan.regularMode, newPlan.count);
511
+
512
+ // Update subscription: change planId, reset expiresAt and startedAt, ensure active status
513
+ await this.subscriptionService.updateFieldsById({
514
+ subscriptionId: existingSubscription.id,
515
+ fields: [
516
+ ['planId', newPlanId],
517
+ ['expiresAt', expiresAt],
518
+ ['startedAt', startedAt],
519
+ ['status', 'active'],
520
+ ],
521
+ });
522
+
523
+ this.logger.info({
524
+ message: 'Subscription upgraded',
525
+ payload: {
526
+ subscriptionId: existingSubscription.id,
527
+ previousPlanId: existingSubscription.planId,
528
+ newPlanId,
529
+ previousExpiresAt: existingSubscription.expiresAt,
530
+ newExpiresAt: expiresAt,
531
+ },
532
+ });
533
+
534
+ // Add new plan's tokens to existing balance (keep existing + add new)
535
+ await this.initializeTokenBalance({
536
+ userId,
537
+ platform,
538
+ planId: newPlanId,
539
+ tokens: newPlan.credits,
540
+ });
541
+
542
+ this.logger.info({
543
+ message: 'Subscription upgrade completed successfully',
544
+ payload: {
545
+ userId,
546
+ platform,
547
+ previousPlanId: currentPlanId,
548
+ newPlanId,
549
+ expiresAt,
550
+ tokensAdded: newPlan.credits,
551
+ },
552
+ });
553
+ }
554
+
555
+ /**
556
+ * Activates a new subscription for the user.
557
+ */
558
+ private async activateNewSubscription(params: {
559
+ userId: string;
560
+ platform: string;
561
+ planId: string;
562
+ startedAt: string;
563
+ expiresAt: string;
564
+ }): Promise<void> {
565
+ const subscriptionData: Omit<SubscriptionEntity, 'id' | 'status'> = {
566
+ userId: params.userId,
567
+ platform: params.platform,
568
+ planId: params.planId,
569
+ startedAt: params.startedAt,
570
+ expiresAt: params.expiresAt,
571
+ provider: 'wayforpay',
572
+ };
573
+
574
+ await this.subscriptionService.activateSubscription(subscriptionData);
575
+
576
+ this.logger.info({
577
+ message: 'New subscription activated',
578
+ payload: {
579
+ userId: params.userId,
580
+ platform: params.platform,
581
+ planId: params.planId,
582
+ },
583
+ });
584
+ }
585
+
586
+ /**
587
+ * Renews an existing subscription by extending its expiration date.
588
+ */
589
+ private async renewSubscription(
590
+ existingSubscription: SubscriptionEntity,
591
+ newExpiresAt: string,
592
+ ): Promise<void> {
593
+ // If the subscription hasn't expired yet, extend from current expiration
594
+ const currentExpiration = moment.utc(existingSubscription.expiresAt);
595
+ const now = moment.utc();
596
+
597
+ let extendedExpiresAt: string;
598
+ if (currentExpiration.isAfter(now)) {
599
+ // Extend from current expiration date
600
+ const durationToAdd = moment.utc(newExpiresAt).diff(moment.utc(), 'milliseconds');
601
+ extendedExpiresAt = currentExpiration.add(durationToAdd, 'milliseconds').toISOString();
602
+ } else {
603
+ // Subscription has expired, use the new expiration date
604
+ extendedExpiresAt = newExpiresAt;
605
+ }
606
+
607
+ await this.subscriptionService.renewSubscription({
608
+ userId: existingSubscription.userId,
609
+ platform: existingSubscription.platform,
610
+ expiresAt: extendedExpiresAt,
611
+ });
612
+
613
+ this.logger.info({
614
+ message: 'Subscription renewed',
615
+ payload: {
616
+ subscriptionId: existingSubscription.id,
617
+ previousExpiresAt: existingSubscription.expiresAt,
618
+ newExpiresAt: extendedExpiresAt,
619
+ },
620
+ });
621
+ }
622
+
623
+ /**
624
+ * Initializes user token balance by creating a token pack.
625
+ */
626
+ private async initializeTokenBalance(params: {
627
+ userId: string;
628
+ platform: string;
629
+ planId: string;
630
+ tokens: number;
631
+ }): Promise<void> {
632
+ const { userId, platform, planId, tokens } = params;
633
+
634
+ await this.tokenPackService.create({
635
+ userId,
636
+ platform,
637
+ packId: planId,
638
+ tokens,
639
+ purchasedAt: moment.utc().toISOString(),
640
+ provider: 'wayforpay',
641
+ });
642
+
643
+ this.logger.info({
644
+ message: 'Token balance initialized',
645
+ payload: {
646
+ userId,
647
+ platform,
648
+ packId: planId,
649
+ tokens,
650
+ },
651
+ });
652
+ }
653
+
654
+ /**
655
+ * Calculates the subscription expiration date based on the regular mode.
656
+ */
657
+ private calculateExpirationDate(
658
+ regularMode: 'daily' | 'monthly' | 'yearly',
659
+ count: number,
660
+ ): string {
661
+ const now = moment.utc();
662
+
663
+ switch (regularMode) {
664
+ case 'daily':
665
+ return now.add(count, 'days').toISOString();
666
+ case 'monthly':
667
+ return now.add(count, 'months').toISOString();
668
+ case 'yearly':
669
+ return now.add(count, 'years').toISOString();
670
+ default:
671
+ return now.add(count, 'months').toISOString();
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Handles failed or declined payments.
677
+ * For recurring subscription renewals, this triggers deactivation of the subscription.
678
+ */
679
+ private async handleFailedPayment(callbackData: WayForPayCallbackData): Promise<void> {
680
+ const { orderReference } = callbackData;
681
+
682
+ const payment = await this.paymentService.getByOrderReference(orderReference);
683
+ if (payment && payment.id) {
684
+ await this.paymentService.changeStatus({
685
+ id: payment.id,
686
+ status: 'failed',
687
+ });
688
+
689
+ this.logger.info({
690
+ message: 'Payment status updated to failed',
691
+ payload: {
692
+ paymentId: payment.id,
693
+ orderReference,
694
+ reason: callbackData.reason,
695
+ },
696
+ });
697
+ }
698
+
699
+ // Handle failed recurring renewal - deactivate subscription
700
+ if (this.isRecurringRenewal(callbackData) && isSubscriptionOrderReference(orderReference)) {
701
+ await this.handleFailedRenewal(callbackData);
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Checks if the payment callback is for a recurring renewal payment.
707
+ * WayForPay sets regularOn field for recurring payments.
708
+ */
709
+ private isRecurringRenewal(callbackData: WayForPayCallbackData): boolean {
710
+ // regularOn is set by WayForPay for recurring payment callbacks
711
+ return !!callbackData.regularOn;
712
+ }
713
+
714
+ /**
715
+ * Handles a failed recurring renewal by cancelling or deactivating the subscription.
716
+ * Respects grace period configuration before full deactivation.
717
+ */
718
+ private async handleFailedRenewal(callbackData: WayForPayCallbackData): Promise<void> {
719
+ const { orderReference, reason, reasonCode } = callbackData;
720
+
721
+ const parsedReference = parseOrderReference(orderReference);
722
+ if (!parsedReference) {
723
+ this.logger.warning({
724
+ message: 'Could not parse order reference for failed renewal',
725
+ payload: { orderReference },
726
+ });
727
+ return;
728
+ }
729
+
730
+ const userId = getOrderReferenceUserId(orderReference);
731
+ const { platform } = parsedReference;
732
+
733
+ if (!userId) {
734
+ this.logger.warning({
735
+ message: 'Could not extract userId from order reference for failed renewal',
736
+ payload: { orderReference },
737
+ });
738
+ return;
739
+ }
740
+
741
+ // Find active subscription for user
742
+ const existingSubscription = await this.subscriptionService.getByUser({
743
+ userId,
744
+ platform,
745
+ status: 'active',
746
+ });
747
+
748
+ if (!existingSubscription) {
749
+ this.logger.info({
750
+ message: 'No active subscription found for failed renewal',
751
+ payload: { userId, platform, orderReference },
752
+ });
753
+ return;
754
+ }
755
+
756
+ // Check if subscription has expired past the grace period
757
+ const now = moment.utc();
758
+ const expiresAt = moment.utc(existingSubscription.expiresAt);
759
+ const gracePeriodMs = this.gracePeriodConfig.enabled ? this.gracePeriodConfig.durationMs : 0;
760
+ const gracePeriodEnd = expiresAt.clone().add(gracePeriodMs, 'milliseconds');
761
+
762
+ if (now.isAfter(gracePeriodEnd)) {
763
+ // Subscription has expired past grace period - deactivate it
764
+ await this.subscriptionService.deactivateSubscription({ id: existingSubscription.id });
765
+
766
+ this.logger.info({
767
+ message: 'Subscription deactivated due to failed renewal (past grace period)',
768
+ payload: {
769
+ subscriptionId: existingSubscription.id,
770
+ userId,
771
+ platform,
772
+ orderReference,
773
+ reason,
774
+ reasonCode,
775
+ expiresAt: existingSubscription.expiresAt,
776
+ gracePeriodMs,
777
+ gracePeriodEnd: gracePeriodEnd.toISOString(),
778
+ },
779
+ });
780
+ } else if (expiresAt.isBefore(now)) {
781
+ // Subscription has expired but within grace period - cancel (mark for future deactivation)
782
+ await this.subscriptionService.cancelSubscription({ id: existingSubscription.id });
783
+
784
+ this.logger.info({
785
+ message: 'Subscription cancelled due to failed renewal (within grace period)',
786
+ payload: {
787
+ subscriptionId: existingSubscription.id,
788
+ userId,
789
+ platform,
790
+ orderReference,
791
+ expiresAt: existingSubscription.expiresAt,
792
+ gracePeriodEnd: gracePeriodEnd.toISOString(),
793
+ reason,
794
+ reasonCode,
795
+ },
796
+ });
797
+ } else {
798
+ this.logger.info({
799
+ message: 'Failed renewal for subscription that has not yet expired',
800
+ payload: {
801
+ subscriptionId: existingSubscription.id,
802
+ userId,
803
+ platform,
804
+ orderReference,
805
+ expiresAt: existingSubscription.expiresAt,
806
+ reason,
807
+ reasonCode,
808
+ },
809
+ });
810
+ }
811
+ }
812
+ }