@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,38 @@
1
+ import { WayForPayCallbackData, WayForPayWebhookResponse } from './wayforpay.types';
2
+
3
+ /**
4
+ * Result of processing a webhook callback
5
+ */
6
+ type WebhookProcessingResult = {
7
+ success: boolean;
8
+ response: WayForPayWebhookResponse;
9
+ error?: string;
10
+ };
11
+
12
+ /**
13
+ * Parameters for activating a subscription from webhook
14
+ */
15
+ type ActivateSubscriptionParams = {
16
+ userId: string;
17
+ platform: string;
18
+ planId: string;
19
+ orderReference: string;
20
+ };
21
+
22
+ /**
23
+ * Interface for webhook handler service operations.
24
+ * Handles payment confirmation webhooks from WayForPay.
25
+ */
26
+ interface IWebhookHandlerService {
27
+ /**
28
+ * Processes a payment confirmation webhook from WayForPay.
29
+ * Verifies signature, updates payment status, activates subscription,
30
+ * and initializes user token balance.
31
+ *
32
+ * @param callbackData - The callback data from WayForPay
33
+ * @returns Processing result with response to send back to WayForPay
34
+ */
35
+ processPaymentWebhook(callbackData: WayForPayCallbackData): Promise<WebhookProcessingResult>;
36
+ }
37
+
38
+ export type { WebhookProcessingResult, ActivateSubscriptionParams, IWebhookHandlerService };
@@ -0,0 +1,317 @@
1
+ import moment from 'moment';
2
+ import { Logger } from '../logger/types';
3
+ import { SubscriptionService } from './service';
4
+ import { SubscriptionPlanService } from '../subscriptionPlan/service';
5
+ import { PaymentService } from '../payments/service';
6
+ import type { ISubscriptionService, SubscriptionEntity, UpgradePreview, UpgradeResult } from './types';
7
+ import type { ISubscriptionPlanService, SubscriptionPlanEntity } from '../subscriptionPlan/types';
8
+ import type { IPaymentService } from '../payments/types';
9
+
10
+ /**
11
+ * Error codes for subscription change operations
12
+ */
13
+ export const SubscriptionChangeErrorCodes = {
14
+ NO_ACTIVE_SUBSCRIPTION: 'NO_ACTIVE_SUBSCRIPTION',
15
+ SUBSCRIPTION_EXPIRED: 'SUBSCRIPTION_EXPIRED',
16
+ SAME_PLAN: 'SAME_PLAN',
17
+ DOWNGRADE_NOT_ALLOWED: 'DOWNGRADE_NOT_ALLOWED',
18
+ UPGRADE_NOT_ALLOWED: 'UPGRADE_NOT_ALLOWED',
19
+ PLAN_NOT_FOUND: 'PLAN_NOT_FOUND',
20
+ CURRENCY_MISMATCH: 'CURRENCY_MISMATCH',
21
+ INVALID_UPGRADE_COST: 'INVALID_UPGRADE_COST',
22
+ } as const;
23
+
24
+ export type SubscriptionChangeErrorCode = (typeof SubscriptionChangeErrorCodes)[keyof typeof SubscriptionChangeErrorCodes];
25
+
26
+ export class SubscriptionChangeError extends Error {
27
+ public readonly code: SubscriptionChangeErrorCode;
28
+
29
+ constructor(code: SubscriptionChangeErrorCode, message: string) {
30
+ super(message);
31
+ this.code = code;
32
+ this.name = 'SubscriptionChangeError';
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Service for handling subscription upgrades and downgrades.
38
+ */
39
+ export class SubscriptionChangeService {
40
+ private readonly logger: Logger;
41
+ private readonly subscriptionService: ISubscriptionService;
42
+ private readonly subscriptionPlanService: ISubscriptionPlanService;
43
+ private readonly paymentService: IPaymentService;
44
+
45
+ constructor({
46
+ logger,
47
+ subscriptionService,
48
+ subscriptionPlanService,
49
+ paymentService,
50
+ }: {
51
+ logger: Logger;
52
+ subscriptionService?: ISubscriptionService;
53
+ subscriptionPlanService?: ISubscriptionPlanService;
54
+ paymentService?: IPaymentService;
55
+ }) {
56
+ this.logger = logger;
57
+ this.subscriptionService = subscriptionService || new SubscriptionService({ logger });
58
+ this.subscriptionPlanService = subscriptionPlanService || new SubscriptionPlanService({ logger });
59
+ this.paymentService = paymentService || new PaymentService({ logger });
60
+ }
61
+
62
+ /**
63
+ * Preview upgrade cost without initiating payment.
64
+ * Calculates the remaining value of the current subscription and the cost to upgrade.
65
+ */
66
+ public async getUpgradePreview(params: {
67
+ userId: string;
68
+ platform: string;
69
+ newPlanId: string;
70
+ }): Promise<UpgradePreview> {
71
+ const { userId, platform, newPlanId } = params;
72
+
73
+ // Get active subscription
74
+ const subscription = await this.subscriptionService.getByUser({
75
+ userId,
76
+ platform,
77
+ status: 'active',
78
+ });
79
+
80
+ if (!subscription) {
81
+ throw new SubscriptionChangeError(
82
+ SubscriptionChangeErrorCodes.NO_ACTIVE_SUBSCRIPTION,
83
+ 'No active subscription found for user',
84
+ );
85
+ }
86
+
87
+ // Check if subscription has expired
88
+ const now = moment.utc();
89
+ const expiresAt = moment.utc(subscription.expiresAt);
90
+ if (expiresAt.isBefore(now)) {
91
+ throw new SubscriptionChangeError(
92
+ SubscriptionChangeErrorCodes.SUBSCRIPTION_EXPIRED,
93
+ 'Current subscription has expired',
94
+ );
95
+ }
96
+
97
+ // Get current and new plans
98
+ const [currentPlan, newPlan] = await Promise.all([
99
+ this.subscriptionPlanService.getById(subscription.planId),
100
+ this.subscriptionPlanService.getById(newPlanId),
101
+ ]);
102
+
103
+ if (!currentPlan) {
104
+ throw new SubscriptionChangeError(
105
+ SubscriptionChangeErrorCodes.PLAN_NOT_FOUND,
106
+ `Current plan not found: ${subscription.planId}`,
107
+ );
108
+ }
109
+
110
+ if (!newPlan) {
111
+ throw new SubscriptionChangeError(
112
+ SubscriptionChangeErrorCodes.PLAN_NOT_FOUND,
113
+ `New plan not found: ${newPlanId}`,
114
+ );
115
+ }
116
+
117
+ // Check if same plan
118
+ if (currentPlan.id === newPlan.id) {
119
+ throw new SubscriptionChangeError(
120
+ SubscriptionChangeErrorCodes.SAME_PLAN,
121
+ 'Cannot upgrade to the same plan',
122
+ );
123
+ }
124
+
125
+ // Check currency match
126
+ if (currentPlan.currency !== newPlan.currency) {
127
+ throw new SubscriptionChangeError(
128
+ SubscriptionChangeErrorCodes.CURRENCY_MISMATCH,
129
+ 'Cannot change between plans with different currencies',
130
+ );
131
+ }
132
+
133
+ // Check if this is actually an upgrade (new plan must be more expensive)
134
+ if (newPlan.amount <= currentPlan.amount) {
135
+ throw new SubscriptionChangeError(
136
+ SubscriptionChangeErrorCodes.DOWNGRADE_NOT_ALLOWED,
137
+ 'New plan must be more expensive than current plan. Use downgrade instead.',
138
+ );
139
+ }
140
+
141
+ // Calculate remaining value
142
+ const remainingValue = this.calculateRemainingValue(subscription, currentPlan);
143
+ const remainingDays = this.calculateRemainingDays(subscription);
144
+
145
+ // Calculate upgrade cost
146
+ const upgradeCost = Math.max(0, Math.round((newPlan.amount - remainingValue) * 100) / 100);
147
+
148
+ // Validate upgrade cost
149
+ if (upgradeCost <= 0) {
150
+ throw new SubscriptionChangeError(
151
+ SubscriptionChangeErrorCodes.INVALID_UPGRADE_COST,
152
+ 'Upgrade cost must be greater than 0',
153
+ );
154
+ }
155
+
156
+ return {
157
+ currentPlan,
158
+ newPlan,
159
+ remainingDays,
160
+ remainingValue: Math.round(remainingValue * 100) / 100,
161
+ upgradeCost,
162
+ currency: currentPlan.currency,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Initiate upgrade - creates payment for the difference.
168
+ * Creates a recurring payment for the new plan.
169
+ */
170
+ public async initiateUpgrade(params: {
171
+ userId: string;
172
+ platform: string;
173
+ newPlanId: string;
174
+ productName: string;
175
+ language?: string;
176
+ }): Promise<UpgradeResult> {
177
+ const { userId, platform, newPlanId, productName, language } = params;
178
+
179
+ // Get upgrade preview (validates everything)
180
+ const preview = await this.getUpgradePreview({ userId, platform, newPlanId });
181
+
182
+ this.logger.info({
183
+ message: 'Initiating subscription upgrade',
184
+ payload: {
185
+ userId,
186
+ platform,
187
+ currentPlanId: preview.currentPlan.id,
188
+ newPlanId: preview.newPlan.id,
189
+ upgradeCost: preview.upgradeCost,
190
+ currency: preview.currency,
191
+ },
192
+ });
193
+
194
+ // Create upgrade payment intent
195
+ const payment = await this.paymentService.createUpgradePaymentIntent({
196
+ userId,
197
+ platform,
198
+ currentPlanId: preview.currentPlan.id,
199
+ newPlanId: preview.newPlan.id,
200
+ productName,
201
+ upgradeCost: preview.upgradeCost,
202
+ currency: preview.currency,
203
+ regularMode: preview.newPlan.regularMode,
204
+ regularCount: preview.newPlan.count,
205
+ language,
206
+ });
207
+
208
+ this.logger.info({
209
+ message: 'Upgrade payment intent created',
210
+ payload: {
211
+ userId,
212
+ platform,
213
+ paymentId: payment.id,
214
+ orderReference: payment.orderReference,
215
+ },
216
+ });
217
+
218
+ return {
219
+ payment,
220
+ paymentUrl: payment.paymentLink,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Simple downgrade - just expire current subscription.
226
+ * User will need to purchase the new subscription themselves.
227
+ */
228
+ public async downgrade(params: {
229
+ userId: string;
230
+ platform: string;
231
+ }): Promise<void> {
232
+ const { userId, platform } = params;
233
+
234
+ // Get active subscription
235
+ const subscription = await this.subscriptionService.getByUser({
236
+ userId,
237
+ platform,
238
+ status: 'active',
239
+ });
240
+
241
+ if (!subscription) {
242
+ throw new SubscriptionChangeError(
243
+ SubscriptionChangeErrorCodes.NO_ACTIVE_SUBSCRIPTION,
244
+ 'No active subscription found for user',
245
+ );
246
+ }
247
+
248
+ this.logger.info({
249
+ message: 'Downgrading subscription (setting to expired)',
250
+ payload: {
251
+ userId,
252
+ platform,
253
+ subscriptionId: subscription.id,
254
+ planId: subscription.planId,
255
+ },
256
+ });
257
+
258
+ // Set subscription status to expired
259
+ await this.subscriptionService.updateFieldsById({
260
+ subscriptionId: subscription.id,
261
+ fields: [['status', 'expired']],
262
+ });
263
+
264
+ this.logger.info({
265
+ message: 'Subscription downgraded successfully',
266
+ payload: {
267
+ userId,
268
+ platform,
269
+ subscriptionId: subscription.id,
270
+ },
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Calculate remaining value of a subscription.
276
+ * Formula: remainingDays * dailyRate
277
+ * Where dailyRate = plan.amount / totalPeriodDays
278
+ */
279
+ private calculateRemainingValue(
280
+ subscription: SubscriptionEntity,
281
+ plan: SubscriptionPlanEntity,
282
+ ): number {
283
+ const remainingDays = this.calculateRemainingDays(subscription);
284
+ const totalPeriodDays = this.getPeriodDays(plan.regularMode, plan.count);
285
+ const dailyRate = plan.amount / totalPeriodDays;
286
+
287
+ return remainingDays * dailyRate;
288
+ }
289
+
290
+ /**
291
+ * Calculate remaining days until subscription expires.
292
+ */
293
+ private calculateRemainingDays(subscription: SubscriptionEntity): number {
294
+ const now = moment.utc();
295
+ const expiresAt = moment.utc(subscription.expiresAt);
296
+ const remainingMs = expiresAt.diff(now);
297
+
298
+ // Convert to days (fractional)
299
+ return Math.max(0, remainingMs / (1000 * 60 * 60 * 24));
300
+ }
301
+
302
+ /**
303
+ * Get the number of days in a subscription period.
304
+ */
305
+ private getPeriodDays(regularMode: 'daily' | 'monthly' | 'yearly', count: number): number {
306
+ switch (regularMode) {
307
+ case 'daily':
308
+ return count;
309
+ case 'monthly':
310
+ return count * 30; // Approximate
311
+ case 'yearly':
312
+ return count * 365; // Approximate
313
+ default:
314
+ return count * 30;
315
+ }
316
+ }
317
+ }
@@ -0,0 +1,9 @@
1
+ import { SubscriptionEntity } from './types';
2
+
3
+ const DEFAULT_SUBSCRIPTION_ENTITY: Partial<SubscriptionEntity> = {
4
+ status: 'active',
5
+ platform: 'telegram',
6
+ provider: 'wayforpay',
7
+ };
8
+
9
+ export { DEFAULT_SUBSCRIPTION_ENTITY };
@@ -0,0 +1,5 @@
1
+ export * from './utils';
2
+ export * from './types';
3
+ export * from './service';
4
+ export * from './status-check.handler';
5
+ export * from './change.service';
@@ -0,0 +1,277 @@
1
+ import { FieldValue, Firestore, getFirestore, Query, QueryDocumentSnapshot } from 'firebase-admin/firestore';
2
+ import { SubscriptionEntity, SubscriptionFieldPath, ISubscriptionRepository } from './types';
3
+ import { isUndefined } from '../../utils';
4
+
5
+ export type UpdateDBSubscriptionFields = [
6
+ SubscriptionFieldPath,
7
+ FieldValue | string | number | boolean | Date | [] | {},
8
+ ][];
9
+
10
+ export class SubscriptionRepository implements ISubscriptionRepository {
11
+ private readonly db: Firestore;
12
+ private readonly collectionName = 'subscriptions';
13
+
14
+ constructor({ db }: { db?: Firestore } = {}) {
15
+ this.db = db || getFirestore();
16
+ }
17
+
18
+ public async getByUser(params: {
19
+ userId: string;
20
+ platform: string;
21
+ status?: SubscriptionEntity['status'] | SubscriptionEntity['status'][];
22
+ }): Promise<SubscriptionEntity | null> {
23
+ const { userId, platform } = params;
24
+
25
+ let query = this.db
26
+ .collection(this.collectionName)
27
+ .where('platform', '==', platform)
28
+ .where('userId', '==', userId);
29
+
30
+ if (!isUndefined(params.status)) {
31
+ // Handle status filter
32
+ if (Array.isArray(params.status)) {
33
+ // Multiple statuses - use 'in' operator
34
+ query = query.where('status', 'in', params.status);
35
+ } else {
36
+ // Single status - use '==' operator
37
+ query = query.where('status', '==', params.status);
38
+ }
39
+ }
40
+
41
+ const querySnapshot = await query.limit(1).get();
42
+
43
+ if (querySnapshot.empty) {
44
+ return null;
45
+ }
46
+
47
+ const doc = querySnapshot.docs[0];
48
+ return this.mapDocumentToEntity(doc);
49
+ }
50
+
51
+ public async getByStatus(params: {
52
+ status: SubscriptionEntity['status'] | SubscriptionEntity['status'][];
53
+ }): Promise<SubscriptionEntity[]> {
54
+ const { status } = params;
55
+
56
+ let query: Query;
57
+
58
+ if (Array.isArray(status)) {
59
+ query = await this.db.collection(this.collectionName).where('status', 'in', status);
60
+ } else {
61
+ query = await this.db.collection(this.collectionName).where('status', '==', status);
62
+ }
63
+
64
+ const querySnapshot = await query.get();
65
+
66
+ if (querySnapshot.empty) {
67
+ return [];
68
+ }
69
+
70
+ return querySnapshot.docs.map((doc) => this.mapDocumentToEntity(doc));
71
+ }
72
+
73
+ public async create(params: {
74
+ userId: string;
75
+ platform: string;
76
+ planId: string;
77
+ expiresAt: string;
78
+ startedAt: string;
79
+ }): Promise<SubscriptionEntity> {
80
+ const { userId, platform, planId, expiresAt, startedAt } = params;
81
+
82
+ const docRef = this.db.collection(this.collectionName).doc();
83
+
84
+ const subscriptionEntity: SubscriptionEntity = {
85
+ id: docRef.id,
86
+ userId,
87
+ platform,
88
+ planId,
89
+ expiresAt,
90
+ startedAt,
91
+ status: 'active',
92
+ provider: 'wayforpay',
93
+ };
94
+
95
+ docRef.set(subscriptionEntity);
96
+
97
+ return subscriptionEntity;
98
+ }
99
+
100
+ public async updateFieldsByUserId(params: {
101
+ userId: string;
102
+ platform: string;
103
+ fields: UpdateDBSubscriptionFields;
104
+ }): Promise<void> {
105
+ const { userId, platform, fields } = params;
106
+
107
+ const updateObject = this.buildUpdateObject(fields);
108
+
109
+ const querySnapshot = await this.db
110
+ .collection(this.collectionName)
111
+ .where('platform', '==', platform)
112
+ .where('userId', '==', userId)
113
+ .limit(1)
114
+ .get();
115
+
116
+ if (querySnapshot.empty) {
117
+ throw new Error(`Subscription not found for userId: ${userId}`);
118
+ }
119
+
120
+ await querySnapshot.docs[0].ref.update(updateObject);
121
+ }
122
+
123
+ public async getExpiredActiveSubscriptions(): Promise<SubscriptionEntity[]> {
124
+ const now = new Date().toISOString();
125
+
126
+ const querySnapshot = await this.db
127
+ .collection(this.collectionName)
128
+ .where('status', '==', 'active')
129
+ .where('expiresAt', '<', now)
130
+ .get();
131
+
132
+ if (querySnapshot.empty) {
133
+ return [];
134
+ }
135
+
136
+ return querySnapshot.docs.map((doc) => this.mapDocumentToEntity(doc));
137
+ }
138
+
139
+ public async updateFieldsById(params: {
140
+ subscriptionId: string;
141
+ fields: UpdateDBSubscriptionFields;
142
+ }): Promise<void> {
143
+ const { subscriptionId, fields } = params;
144
+
145
+ const updateObject = this.buildUpdateObject(fields);
146
+
147
+ await this.db.collection(this.collectionName).doc(subscriptionId).update(updateObject);
148
+ }
149
+
150
+ private mapDocumentToEntity(doc: QueryDocumentSnapshot): SubscriptionEntity {
151
+ return {
152
+ id: doc.id,
153
+ ...doc.data(),
154
+ } as SubscriptionEntity;
155
+ }
156
+
157
+ public async activateSubscription(params: Omit<SubscriptionEntity, 'id' | 'status'>): Promise<void> {
158
+ const { userId, platform } = params;
159
+ return await this.db.runTransaction(async (tx) => {
160
+ // Get user document
161
+ const userSnapshot = await tx.get(this.db.collection(`platform/${platform}/users`).doc(userId));
162
+
163
+ if (!userSnapshot.exists) {
164
+ throw new Error(`User not found: ${userId}`);
165
+ }
166
+
167
+ const docRef = this.db.collection(this.collectionName).doc();
168
+ const newSubscription: SubscriptionEntity = {
169
+ ...params,
170
+ id: docRef.id,
171
+ status: 'active',
172
+ };
173
+
174
+ await tx.create(docRef, newSubscription);
175
+
176
+ // Update user document
177
+ await tx.update(userSnapshot.ref, {
178
+ 'subscription.isActive': true,
179
+ 'subscription.isTrial': false,
180
+ });
181
+ });
182
+ }
183
+
184
+ public async deactivateSubscription({ id }: Pick<SubscriptionEntity, 'id'>): Promise<void> {
185
+ return await this.db.runTransaction(async (tx) => {
186
+ const subscriptionSnapshot = await tx.get(this.db.collection(this.collectionName).doc(id));
187
+
188
+ if (!subscriptionSnapshot.exists) {
189
+ throw new Error(`Subscription not found for id: ${id}`);
190
+ }
191
+
192
+ const subscription = subscriptionSnapshot.data() as SubscriptionEntity;
193
+ const userSnapshot = await tx.get(
194
+ this.db.collection(`platform/${subscription.platform}/users`).doc(subscription.userId),
195
+ );
196
+
197
+ if (!userSnapshot.exists) {
198
+ throw new Error(`User not found: ${subscription.userId}; platform: ${subscription.platform}`);
199
+ }
200
+
201
+ const newSubscriptionData: Pick<SubscriptionEntity, 'status'> = {
202
+ status: 'expired',
203
+ };
204
+
205
+ await tx.update(subscriptionSnapshot.ref, newSubscriptionData);
206
+ await tx.update(userSnapshot.ref, {
207
+ 'subscription.isActive': false,
208
+ });
209
+ });
210
+ }
211
+
212
+ public async cancelSubscription({ id }: Pick<SubscriptionEntity, 'id'>): Promise<void> {
213
+ return await this.db.runTransaction(async (tx) => {
214
+ const subscriptionSnapshot = await tx.get(this.db.collection(this.collectionName).doc(id));
215
+
216
+ if (!subscriptionSnapshot.exists) {
217
+ throw new Error(`Subscription not found for id: ${id}`);
218
+ }
219
+
220
+ const newSubscriptionData: Pick<SubscriptionEntity, 'status'> = {
221
+ status: 'cancelled',
222
+ };
223
+
224
+ await tx.update(subscriptionSnapshot.ref, newSubscriptionData);
225
+ });
226
+ }
227
+
228
+ public async renewSubscription(
229
+ params: Pick<SubscriptionEntity, 'userId' | 'platform' | 'expiresAt'>,
230
+ ): Promise<void> {
231
+ const { userId, platform } = params;
232
+ return await this.db.runTransaction(async (tx) => {
233
+ // Get user document
234
+ const userSnapshot = await tx.get(this.db.collection(`platform/${platform}/users`).doc(userId));
235
+ // Get subscription document
236
+ const subscriptionSnapshot = await tx.get(
237
+ this.db
238
+ .collection(this.collectionName)
239
+ .where('userId', '==', userId)
240
+ .where('platform', '==', platform)
241
+ .where('status', '==', 'active')
242
+ .limit(1),
243
+ );
244
+
245
+ if (subscriptionSnapshot.empty) {
246
+ throw new Error(`Subscription not found for userId: ${userId}`);
247
+ }
248
+
249
+ if (!userSnapshot.exists) {
250
+ throw new Error(`User not found: ${userId}`);
251
+ }
252
+
253
+ const updateObject: Pick<SubscriptionEntity, 'status' | 'expiresAt'> = {
254
+ status: 'active',
255
+ expiresAt: params.expiresAt,
256
+ };
257
+
258
+ await tx.update(subscriptionSnapshot.docs[0].ref, updateObject);
259
+
260
+ // Update user document
261
+ await tx.update(userSnapshot.ref, {
262
+ 'subscription.isActive': true,
263
+ 'subscription.isTrial': false,
264
+ });
265
+ });
266
+ }
267
+
268
+ private buildUpdateObject(fields: UpdateDBSubscriptionFields): Record<string, any> {
269
+ const updateObject: Record<string, any> = {};
270
+
271
+ fields.forEach(([fieldPath, value]) => {
272
+ updateObject[fieldPath] = value;
273
+ });
274
+
275
+ return updateObject;
276
+ }
277
+ }