@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,297 @@
1
+ import moment from 'moment';
2
+ import { Logger } from '../logger/types';
3
+ import { TokenPackService } from '../tokenPack/service';
4
+ import { PaymentService } from '../payments/service';
5
+ import { ITokenPackService } from '../tokenPack/types';
6
+ import { IPaymentService } from '../payments/types';
7
+ import { DEFAULT_TOKEN_PACK_PLANS } from './const';
8
+ import { createTokenPackPaymentAPI } from '../payments/api';
9
+ import type {
10
+ TokenPackPlan,
11
+ PurchaseUrlResult,
12
+ CreatePurchaseUrlParams,
13
+ AddTokensParams,
14
+ DeductTokensParams,
15
+ TokenBalance,
16
+ ITokenService,
17
+ } from './types';
18
+
19
+ /**
20
+ * TokenService provides business logic for token pack purchases and balance management.
21
+ * Works independently of subscription status.
22
+ */
23
+ export class TokenService implements ITokenService {
24
+ private readonly logger: Logger;
25
+ private readonly tokenPackService: ITokenPackService;
26
+ private readonly paymentService: IPaymentService;
27
+ private readonly tokenPackPlans: TokenPackPlan[];
28
+
29
+ constructor({
30
+ logger,
31
+ tokenPackService,
32
+ paymentService,
33
+ tokenPackPlans,
34
+ }: {
35
+ logger: Logger;
36
+ tokenPackService?: ITokenPackService;
37
+ paymentService?: IPaymentService;
38
+ tokenPackPlans?: TokenPackPlan[];
39
+ }) {
40
+ this.logger = logger;
41
+ this.tokenPackService = tokenPackService || new TokenPackService({ logger });
42
+ this.paymentService = paymentService || new PaymentService({ logger });
43
+ this.tokenPackPlans = tokenPackPlans || DEFAULT_TOKEN_PACK_PLANS;
44
+ }
45
+
46
+ /**
47
+ * Returns all available token pack plans for purchase.
48
+ */
49
+ public getAvailablePacks(): TokenPackPlan[] {
50
+ return this.tokenPackPlans.filter((plan) => plan.isActive);
51
+ }
52
+
53
+ /**
54
+ * Creates a one-time payment URL for purchasing a token pack.
55
+ */
56
+ public async createPurchaseUrl(params: CreatePurchaseUrlParams): Promise<PurchaseUrlResult | null> {
57
+ const { userId, platform, packId, language = 'UA', returnUrl } = params;
58
+
59
+ try {
60
+ // Find the token pack plan
61
+ const plan = this.tokenPackPlans.find((p) => p.id === packId);
62
+ if (!plan) {
63
+ this.logger.error({
64
+ message: 'Token pack plan not found',
65
+ payload: { packId },
66
+ });
67
+ return null;
68
+ }
69
+
70
+ if (!plan.isActive) {
71
+ this.logger.error({
72
+ message: 'Token pack plan is not active',
73
+ payload: { packId },
74
+ });
75
+ return null;
76
+ }
77
+
78
+ // Create one-time payment URL via WayForPay API
79
+ // Uses the v2 order reference format for token packs (tkn type)
80
+ const paymentResult = await createTokenPackPaymentAPI({
81
+ userId,
82
+ platform,
83
+ productName: `Token Pack - ${plan.tokens} tokens`,
84
+ productPrice: plan.amount,
85
+ packId,
86
+ currency: plan.currency,
87
+ language,
88
+ returnUrl,
89
+ });
90
+
91
+ if (!paymentResult) {
92
+ this.logger.error({
93
+ message: 'Failed to generate payment URL for token pack',
94
+ payload: { userId, platform, packId },
95
+ });
96
+ return null;
97
+ }
98
+
99
+ // Create payment record in database
100
+ await this.paymentService.create({
101
+ orderReference: paymentResult.orderReference,
102
+ userId,
103
+ status: 'pending',
104
+ paymentLink: paymentResult.url,
105
+ planId: packId,
106
+ paymentType: 'token_pack',
107
+ amount: plan.amount,
108
+ currency: plan.currency,
109
+ createdAt: moment.utc().toISOString(),
110
+ platform,
111
+ provider: 'wayforpay',
112
+ });
113
+
114
+ this.logger.info({
115
+ message: 'Token pack purchase URL created',
116
+ payload: {
117
+ userId,
118
+ platform,
119
+ packId,
120
+ orderReference: paymentResult.orderReference,
121
+ },
122
+ });
123
+
124
+ return {
125
+ url: paymentResult.url,
126
+ orderReference: paymentResult.orderReference,
127
+ };
128
+ } catch (error) {
129
+ this.logger.error({
130
+ message: 'Error creating token pack purchase URL',
131
+ payload: {
132
+ userId,
133
+ platform,
134
+ packId,
135
+ error: JSON.stringify(error),
136
+ },
137
+ });
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Adds tokens to a user's balance by creating a new token pack.
144
+ * Called after successful payment confirmation.
145
+ */
146
+ public async addTokensToUser(params: AddTokensParams): Promise<void> {
147
+ const { userId, platform, packId, tokens } = params;
148
+
149
+ try {
150
+ await this.tokenPackService.create({
151
+ userId,
152
+ platform,
153
+ packId,
154
+ tokens,
155
+ purchasedAt: moment.utc().toISOString(),
156
+ provider: 'wayforpay',
157
+ });
158
+
159
+ this.logger.info({
160
+ message: 'Tokens added to user balance',
161
+ payload: {
162
+ userId,
163
+ platform,
164
+ packId,
165
+ tokens,
166
+ },
167
+ });
168
+ } catch (error) {
169
+ this.logger.error({
170
+ message: 'Error adding tokens to user balance',
171
+ payload: {
172
+ userId,
173
+ platform,
174
+ packId,
175
+ tokens,
176
+ error: JSON.stringify(error),
177
+ },
178
+ });
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Deducts tokens from a user's available balance.
185
+ * Deducts from oldest packs first (FIFO).
186
+ */
187
+ public async deductTokens(params: DeductTokensParams): Promise<void> {
188
+ const { userId, platform, amount } = params;
189
+
190
+ try {
191
+ // Get active token packs sorted by purchase date (oldest first)
192
+ const activePacks = await this.tokenPackService.getByUser({
193
+ userId,
194
+ platform,
195
+ status: 'active',
196
+ });
197
+
198
+ if (activePacks.length === 0) {
199
+ throw new Error(`No active token packs found for user ${userId}`);
200
+ }
201
+
202
+ // Sort by purchasedAt (oldest first) for FIFO deduction
203
+ const sortedPacks = activePacks.sort(
204
+ (a, b) => new Date(a.purchasedAt).getTime() - new Date(b.purchasedAt).getTime(),
205
+ );
206
+
207
+ // Calculate total available tokens
208
+ const totalAvailable = sortedPacks.reduce((sum, pack) => sum + pack.tokensRemaining, 0);
209
+
210
+ if (totalAvailable < amount) {
211
+ throw new Error(
212
+ `Insufficient tokens. Required: ${amount}, Available: ${totalAvailable}`,
213
+ );
214
+ }
215
+
216
+ // Deduct tokens from packs (FIFO)
217
+ let remainingToDeduct = amount;
218
+
219
+ for (const pack of sortedPacks) {
220
+ if (remainingToDeduct <= 0) break;
221
+
222
+ const deductFromPack = Math.min(pack.tokensRemaining, remainingToDeduct);
223
+ await this.tokenPackService.deductTokens({
224
+ id: pack.id,
225
+ amount: deductFromPack,
226
+ });
227
+
228
+ remainingToDeduct -= deductFromPack;
229
+ }
230
+
231
+ this.logger.info({
232
+ message: 'Tokens deducted from user balance',
233
+ payload: {
234
+ userId,
235
+ platform,
236
+ amount,
237
+ remainingTotal: totalAvailable - amount,
238
+ },
239
+ });
240
+ } catch (error) {
241
+ this.logger.error({
242
+ message: 'Error deducting tokens from user balance',
243
+ payload: {
244
+ userId,
245
+ platform,
246
+ amount,
247
+ error: JSON.stringify(error),
248
+ },
249
+ });
250
+ throw error;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Gets the current token balance for a user.
256
+ */
257
+ public async getBalance(params: { userId: string; platform: string }): Promise<TokenBalance> {
258
+ const { userId, platform } = params;
259
+
260
+ try {
261
+ const activePacks = await this.tokenPackService.getByUser({
262
+ userId,
263
+ platform,
264
+ status: 'active',
265
+ });
266
+
267
+ const available = activePacks.reduce((sum, pack) => sum + pack.tokensRemaining, 0);
268
+
269
+ const packs = activePacks.map((pack) => ({
270
+ id: pack.id,
271
+ packId: pack.packId,
272
+ tokensRemaining: pack.tokensRemaining,
273
+ purchasedAt: pack.purchasedAt,
274
+ }));
275
+
276
+ return {
277
+ available,
278
+ packs,
279
+ };
280
+ } catch (error) {
281
+ this.logger.error({
282
+ message: 'Error getting token balance',
283
+ payload: {
284
+ userId,
285
+ platform,
286
+ error: JSON.stringify(error),
287
+ },
288
+ });
289
+
290
+ // Return empty balance on error
291
+ return {
292
+ available: 0,
293
+ packs: [],
294
+ };
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Token pack plan definition for one-time purchase.
3
+ */
4
+ type TokenPackPlan = {
5
+ /** Unique identifier for the token pack plan */
6
+ id: string;
7
+ /** Translation key for the pack title */
8
+ titleCode: string;
9
+ /** Translation key for the pack description */
10
+ descriptionCode: string;
11
+ /** Number of tokens in the pack */
12
+ tokens: number;
13
+ /** Price amount */
14
+ amount: number;
15
+ /** Currency for the price */
16
+ currency: 'UAH' | 'USD';
17
+ /** Whether this pack is currently available for purchase */
18
+ isActive: boolean;
19
+ };
20
+
21
+ /**
22
+ * Result of creating a purchase URL.
23
+ */
24
+ type PurchaseUrlResult = {
25
+ /** The payment URL to redirect the user to */
26
+ url: string;
27
+ /** Unique order reference for tracking the payment */
28
+ orderReference: string;
29
+ };
30
+
31
+ /**
32
+ * Parameters for creating a token pack purchase URL.
33
+ */
34
+ type CreatePurchaseUrlParams = {
35
+ userId: string;
36
+ platform: string;
37
+ packId: string;
38
+ language?: 'EN' | 'UA';
39
+ returnUrl?: string;
40
+ };
41
+
42
+ /**
43
+ * Parameters for adding tokens to a user's balance.
44
+ */
45
+ type AddTokensParams = {
46
+ userId: string;
47
+ platform: string;
48
+ packId: string;
49
+ tokens: number;
50
+ };
51
+
52
+ /**
53
+ * Parameters for deducting tokens from a user's balance.
54
+ */
55
+ type DeductTokensParams = {
56
+ userId: string;
57
+ platform: string;
58
+ amount: number;
59
+ };
60
+
61
+ /**
62
+ * Token balance information for a user.
63
+ */
64
+ type TokenBalance = {
65
+ /** Total available tokens across all active packs */
66
+ available: number;
67
+ /** List of active token packs with remaining tokens */
68
+ packs: {
69
+ id: string;
70
+ packId: string;
71
+ tokensRemaining: number;
72
+ purchasedAt: string;
73
+ }[];
74
+ };
75
+
76
+ /**
77
+ * Interface for token service operations.
78
+ * Provides methods for token pack management and balance operations.
79
+ */
80
+ interface ITokenService {
81
+ /**
82
+ * Returns all available token pack plans for purchase.
83
+ * @returns Array of active token pack plans
84
+ */
85
+ getAvailablePacks(): TokenPackPlan[];
86
+
87
+ /**
88
+ * Creates a one-time payment URL for purchasing a token pack.
89
+ * @param params - Purchase parameters
90
+ * @returns Promise resolving to payment URL and order reference
91
+ */
92
+ createPurchaseUrl(params: CreatePurchaseUrlParams): Promise<PurchaseUrlResult | null>;
93
+
94
+ /**
95
+ * Adds tokens to a user's balance by creating a new token pack.
96
+ * Called after successful payment confirmation.
97
+ * @param params - Token addition parameters
98
+ */
99
+ addTokensToUser(params: AddTokensParams): Promise<void>;
100
+
101
+ /**
102
+ * Deducts tokens from a user's available balance.
103
+ * Deducts from oldest packs first (FIFO).
104
+ * @param params - Deduction parameters
105
+ */
106
+ deductTokens(params: DeductTokensParams): Promise<void>;
107
+
108
+ /**
109
+ * Gets the current token balance for a user.
110
+ * @param params - User identification parameters
111
+ * @returns Token balance information
112
+ */
113
+ getBalance(params: { userId: string; platform: string }): Promise<TokenBalance>;
114
+ }
115
+
116
+ export type {
117
+ TokenPackPlan,
118
+ PurchaseUrlResult,
119
+ CreatePurchaseUrlParams,
120
+ AddTokensParams,
121
+ DeductTokensParams,
122
+ TokenBalance,
123
+ ITokenService,
124
+ };
@@ -0,0 +1,9 @@
1
+ import { TokenPackEntity } from './types';
2
+
3
+ const DEFAULT_TOKEN_PACK_ENTITY: Partial<TokenPackEntity> = {
4
+ status: 'active',
5
+ platform: 'telegram',
6
+ provider: 'wayforpay',
7
+ };
8
+
9
+ export { DEFAULT_TOKEN_PACK_ENTITY };
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './service';
3
+ export * from './repository';
4
+ export * from './const';
@@ -0,0 +1,144 @@
1
+ import { FieldValue, Firestore, getFirestore, QueryDocumentSnapshot } from 'firebase-admin/firestore';
2
+ import { TokenPackEntity, TokenPackFieldPath, ITokenPackRepository } from './types';
3
+ import { isUndefined } from '../../utils';
4
+
5
+ export type UpdateDBTokenPackFields = [
6
+ TokenPackFieldPath,
7
+ FieldValue | string | number | boolean | Date | [] | {},
8
+ ][];
9
+
10
+ export class TokenPackRepository implements ITokenPackRepository {
11
+ private readonly db: Firestore;
12
+ private readonly collectionName = 'tokenPacks';
13
+
14
+ constructor({ db }: { db?: Firestore } = {}) {
15
+ this.db = db || getFirestore();
16
+ }
17
+
18
+ public async getById(id: string): Promise<TokenPackEntity | null> {
19
+ const doc = await this.db.collection(this.collectionName).doc(id).get();
20
+
21
+ if (!doc.exists) {
22
+ return null;
23
+ }
24
+
25
+ return this.mapDocumentToEntity(doc as QueryDocumentSnapshot);
26
+ }
27
+
28
+ public async getByUser(params: {
29
+ userId: string;
30
+ platform: string;
31
+ status?: TokenPackEntity['status'] | TokenPackEntity['status'][];
32
+ }): Promise<TokenPackEntity[]> {
33
+ const { userId, platform } = params;
34
+
35
+ let query = this.db
36
+ .collection(this.collectionName)
37
+ .where('platform', '==', platform)
38
+ .where('userId', '==', userId);
39
+
40
+ if (!isUndefined(params.status)) {
41
+ if (Array.isArray(params.status)) {
42
+ query = query.where('status', 'in', params.status);
43
+ } else {
44
+ query = query.where('status', '==', params.status);
45
+ }
46
+ }
47
+
48
+ const querySnapshot = await query.get();
49
+
50
+ if (querySnapshot.empty) {
51
+ return [];
52
+ }
53
+
54
+ return querySnapshot.docs.map((doc) => this.mapDocumentToEntity(doc));
55
+ }
56
+
57
+ public async create(
58
+ params: Omit<TokenPackEntity, 'id' | 'status' | 'tokensRemaining'>,
59
+ ): Promise<TokenPackEntity> {
60
+ const docRef = this.db.collection(this.collectionName).doc();
61
+
62
+ const tokenPackEntity: TokenPackEntity = {
63
+ ...params,
64
+ id: docRef.id,
65
+ status: 'active',
66
+ tokensRemaining: params.tokens,
67
+ };
68
+
69
+ await docRef.set(tokenPackEntity);
70
+
71
+ return tokenPackEntity;
72
+ }
73
+
74
+ public async updateFieldsById(params: { id: string; fields: UpdateDBTokenPackFields }): Promise<void> {
75
+ const { id, fields } = params;
76
+
77
+ const updateObject = this.buildUpdateObject(fields);
78
+
79
+ await this.db.collection(this.collectionName).doc(id).update(updateObject);
80
+ }
81
+
82
+ public async deductTokens(params: { id: string; amount: number }): Promise<void> {
83
+ const { id, amount } = params;
84
+
85
+ return await this.db.runTransaction(async (tx) => {
86
+ const docRef = this.db.collection(this.collectionName).doc(id);
87
+ const doc = await tx.get(docRef);
88
+
89
+ if (!doc.exists) {
90
+ throw new Error(`TokenPack not found: ${id}`);
91
+ }
92
+
93
+ const tokenPack = doc.data() as TokenPackEntity;
94
+ const newRemaining = tokenPack.tokensRemaining - amount;
95
+
96
+ if (newRemaining < 0) {
97
+ throw new Error(`Insufficient tokens in pack ${id}`);
98
+ }
99
+
100
+ const updateData: Partial<TokenPackEntity> = {
101
+ tokensRemaining: newRemaining,
102
+ };
103
+
104
+ if (newRemaining === 0) {
105
+ updateData.status = 'depleted';
106
+ }
107
+
108
+ await tx.update(docRef, updateData);
109
+ });
110
+ }
111
+
112
+ public async getExpiredActiveTokenPacks(): Promise<TokenPackEntity[]> {
113
+ const now = new Date().toISOString();
114
+
115
+ const querySnapshot = await this.db
116
+ .collection(this.collectionName)
117
+ .where('status', '==', 'active')
118
+ .where('expiresAt', '<', now)
119
+ .get();
120
+
121
+ if (querySnapshot.empty) {
122
+ return [];
123
+ }
124
+
125
+ return querySnapshot.docs.map((doc) => this.mapDocumentToEntity(doc));
126
+ }
127
+
128
+ private mapDocumentToEntity(doc: QueryDocumentSnapshot): TokenPackEntity {
129
+ return {
130
+ id: doc.id,
131
+ ...doc.data(),
132
+ } as TokenPackEntity;
133
+ }
134
+
135
+ private buildUpdateObject(fields: UpdateDBTokenPackFields): Record<string, any> {
136
+ const updateObject: Record<string, any> = {};
137
+
138
+ fields.forEach(([fieldPath, value]) => {
139
+ updateObject[fieldPath] = value;
140
+ });
141
+
142
+ return updateObject;
143
+ }
144
+ }
@@ -0,0 +1,119 @@
1
+ import { Logger } from '../logger/types';
2
+ import { TokenPackRepository, UpdateDBTokenPackFields } from './repository';
3
+ import { TokenPackEntity, ITokenPackRepository, ITokenPackService } from './types';
4
+
5
+ export class TokenPackService implements ITokenPackService {
6
+ private readonly logger: Logger;
7
+ private readonly repository: ITokenPackRepository;
8
+
9
+ constructor({
10
+ logger,
11
+ repository,
12
+ }: {
13
+ logger: Logger;
14
+ repository?: ITokenPackRepository;
15
+ }) {
16
+ this.logger = logger;
17
+ this.repository = repository || new TokenPackRepository();
18
+ }
19
+
20
+ public async getById(id: string): Promise<TokenPackEntity | null> {
21
+ try {
22
+ return await this.repository.getById(id);
23
+ } catch (error) {
24
+ this.logger.error({
25
+ message: 'Error in token pack service getById',
26
+ payload: {
27
+ id,
28
+ error: JSON.stringify(error),
29
+ },
30
+ });
31
+ return null;
32
+ }
33
+ }
34
+
35
+ public async getByUser(params: {
36
+ userId: string;
37
+ platform: string;
38
+ status?: TokenPackEntity['status'] | TokenPackEntity['status'][];
39
+ }): Promise<TokenPackEntity[]> {
40
+ try {
41
+ return await this.repository.getByUser(params);
42
+ } catch (error) {
43
+ this.logger.error({
44
+ message: 'Error in token pack service getByUser',
45
+ payload: {
46
+ userId: params.userId,
47
+ platform: params.platform,
48
+ error: JSON.stringify(error),
49
+ },
50
+ });
51
+ return [];
52
+ }
53
+ }
54
+
55
+ public async create(
56
+ params: Omit<TokenPackEntity, 'id' | 'status' | 'tokensRemaining'>,
57
+ ): Promise<TokenPackEntity> {
58
+ try {
59
+ return await this.repository.create(params);
60
+ } catch (error) {
61
+ this.logger.error({
62
+ message: 'Error in token pack service create',
63
+ payload: {
64
+ userId: params.userId,
65
+ platform: params.platform,
66
+ packId: params.packId,
67
+ error: JSON.stringify(error),
68
+ },
69
+ });
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ public async updateFieldsById(params: { id: string; fields: UpdateDBTokenPackFields }): Promise<void> {
75
+ try {
76
+ await this.repository.updateFieldsById(params);
77
+ } catch (error) {
78
+ this.logger.error({
79
+ message: 'Error in token pack service updateFieldsById',
80
+ payload: {
81
+ id: params.id,
82
+ fields: JSON.stringify(params.fields),
83
+ error: JSON.stringify(error),
84
+ },
85
+ });
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ public async deductTokens(params: { id: string; amount: number }): Promise<void> {
91
+ try {
92
+ await this.repository.deductTokens(params);
93
+ } catch (error) {
94
+ this.logger.error({
95
+ message: 'Error in token pack service deductTokens',
96
+ payload: {
97
+ id: params.id,
98
+ amount: params.amount,
99
+ error: JSON.stringify(error),
100
+ },
101
+ });
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ public async getExpiredActiveTokenPacks(): Promise<TokenPackEntity[]> {
107
+ try {
108
+ return await this.repository.getExpiredActiveTokenPacks();
109
+ } catch (error) {
110
+ this.logger.error({
111
+ message: 'Error in token pack service getExpiredActiveTokenPacks',
112
+ payload: {
113
+ error: JSON.stringify(error),
114
+ },
115
+ });
116
+ return [];
117
+ }
118
+ }
119
+ }