@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,499 @@
1
+ import { describe, it, beforeEach, mock } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { TokenService } from './service';
4
+ import type { TokenPackPlan } from './types';
5
+ import type { ITokenPackService, TokenPackEntity } from '../tokenPack/types';
6
+ import type { IPaymentService, PaymentEntity } from '../payments/types';
7
+ import type { Logger } from '../logger/types';
8
+
9
+ // Mock logger
10
+ const createMockLogger = (): Logger => ({
11
+ info: mock.fn(),
12
+ warning: mock.fn(),
13
+ error: mock.fn(),
14
+ debug: mock.fn(),
15
+ });
16
+
17
+ // Mock token pack plan
18
+ const createMockTokenPackPlan = (overrides: Partial<TokenPackPlan> = {}): TokenPackPlan => ({
19
+ id: 'token-pack-small',
20
+ titleCode: 'token.pack.small.title',
21
+ descriptionCode: 'token.pack.small.description',
22
+ tokens: 10000,
23
+ amount: 49,
24
+ currency: 'UAH',
25
+ isActive: true,
26
+ ...overrides,
27
+ });
28
+
29
+ // Mock token pack entity
30
+ const createMockTokenPackEntity = (overrides: Partial<TokenPackEntity> = {}): TokenPackEntity => ({
31
+ id: 'pack-123',
32
+ userId: 'user-123',
33
+ platform: 'telegram',
34
+ packId: 'token-pack-small',
35
+ tokens: 10000,
36
+ tokensRemaining: 10000,
37
+ status: 'active',
38
+ purchasedAt: '2025-01-01T00:00:00.000Z',
39
+ provider: 'wayforpay',
40
+ ...overrides,
41
+ });
42
+
43
+ // Mock payment entity
44
+ const createMockPayment = (overrides: Partial<PaymentEntity> = {}): PaymentEntity => ({
45
+ id: 'pay-123',
46
+ orderReference: 'order-123',
47
+ userId: 'user-123',
48
+ platform: 'telegram',
49
+ status: 'pending',
50
+ paymentLink: 'https://pay.example.com/link',
51
+ planId: 'token-pack-small',
52
+ paymentType: 'token_pack',
53
+ amount: 49,
54
+ currency: 'UAH',
55
+ createdAt: new Date().toISOString(),
56
+ provider: 'wayforpay',
57
+ ...overrides,
58
+ });
59
+
60
+ // Mock token pack service
61
+ const createMockTokenPackService = (overrides: Partial<ITokenPackService> = {}): ITokenPackService => ({
62
+ getById: mock.fn(async () => null),
63
+ getByUser: mock.fn(async () => []),
64
+ create: mock.fn(async (params) => ({
65
+ id: 'new-pack-id',
66
+ ...params,
67
+ status: 'active',
68
+ tokensRemaining: params.tokens,
69
+ })),
70
+ updateFieldsById: mock.fn(async () => {}),
71
+ deductTokens: mock.fn(async () => {}),
72
+ getExpiredActiveTokenPacks: mock.fn(async () => []),
73
+ ...overrides,
74
+ });
75
+
76
+ // Mock payment service
77
+ const createMockPaymentService = (overrides: Partial<IPaymentService> = {}): IPaymentService => ({
78
+ getByOrderReference: mock.fn(async () => null),
79
+ getByUser: mock.fn(async () => []),
80
+ create: mock.fn(async (data) => ({ id: 'pay-id', ...data })),
81
+ updateStatus: mock.fn(async () => {}),
82
+ changeStatus: mock.fn(async () => {}),
83
+ getExpiredPendingPayments: mock.fn(async () => []),
84
+ createPaymentIntent: mock.fn(async () => createMockPayment()),
85
+ createTokenPackPaymentIntent: mock.fn(async () => createMockPayment()),
86
+ createUpgradePaymentIntent: mock.fn(async () => createMockPayment()),
87
+ validateSignature: mock.fn(() => ({ isValid: true })),
88
+ ...overrides,
89
+ });
90
+
91
+ describe('TokenService', () => {
92
+ let logger: Logger;
93
+ let tokenPackService: ITokenPackService;
94
+ let paymentService: IPaymentService;
95
+ let tokenPackPlans: TokenPackPlan[];
96
+ let service: TokenService;
97
+
98
+ beforeEach(() => {
99
+ logger = createMockLogger();
100
+ tokenPackService = createMockTokenPackService();
101
+ paymentService = createMockPaymentService();
102
+ tokenPackPlans = [
103
+ createMockTokenPackPlan({ id: 'token-pack-small', tokens: 10000, amount: 49, isActive: true }),
104
+ createMockTokenPackPlan({ id: 'token-pack-medium', tokens: 30000, amount: 129, isActive: true }),
105
+ createMockTokenPackPlan({ id: 'token-pack-large', tokens: 75000, amount: 299, isActive: false }),
106
+ ];
107
+ service = new TokenService({
108
+ logger,
109
+ tokenPackService,
110
+ paymentService,
111
+ tokenPackPlans,
112
+ });
113
+ });
114
+
115
+ describe('getAvailablePacks', () => {
116
+ it('should return only active packs', () => {
117
+ const result = service.getAvailablePacks();
118
+
119
+ assert.strictEqual(result.length, 2);
120
+ assert.ok(result.every((pack) => pack.isActive));
121
+ });
122
+
123
+ it('should return empty array if no active packs', () => {
124
+ service = new TokenService({
125
+ logger,
126
+ tokenPackService,
127
+ paymentService,
128
+ tokenPackPlans: [createMockTokenPackPlan({ isActive: false })],
129
+ });
130
+
131
+ const result = service.getAvailablePacks();
132
+
133
+ assert.strictEqual(result.length, 0);
134
+ });
135
+ });
136
+
137
+ describe('createPurchaseUrl', () => {
138
+ it('should return null if pack not found', async () => {
139
+ const result = await service.createPurchaseUrl({
140
+ userId: 'user-123',
141
+ platform: 'telegram',
142
+ packId: 'non-existent-pack',
143
+ });
144
+
145
+ assert.strictEqual(result, null);
146
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
147
+ });
148
+
149
+ it('should return null if pack is not active', async () => {
150
+ const result = await service.createPurchaseUrl({
151
+ userId: 'user-123',
152
+ platform: 'telegram',
153
+ packId: 'token-pack-large', // This pack is inactive
154
+ });
155
+
156
+ assert.strictEqual(result, null);
157
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
158
+ });
159
+
160
+ it('should return null on API error', async () => {
161
+ // The service uses createTokenPackPaymentAPI which is harder to mock
162
+ // This test would require mocking the api module
163
+ // For now, we test the error handling path
164
+ service = new TokenService({
165
+ logger,
166
+ tokenPackService,
167
+ paymentService: createMockPaymentService({
168
+ create: mock.fn(async () => {
169
+ throw new Error('API error');
170
+ }),
171
+ }),
172
+ tokenPackPlans,
173
+ });
174
+
175
+ // The actual API call happens before create, so this test is limited
176
+ });
177
+ });
178
+
179
+ describe('addTokensToUser', () => {
180
+ it('should create token pack for user', async () => {
181
+ const createMock = mock.fn(async (params) => ({
182
+ id: 'new-pack-id',
183
+ ...params,
184
+ status: 'active',
185
+ tokensRemaining: params.tokens,
186
+ }));
187
+ tokenPackService = createMockTokenPackService({ create: createMock });
188
+ service = new TokenService({
189
+ logger,
190
+ tokenPackService,
191
+ paymentService,
192
+ tokenPackPlans,
193
+ });
194
+
195
+ await service.addTokensToUser({
196
+ userId: 'user-123',
197
+ platform: 'telegram',
198
+ packId: 'token-pack-small',
199
+ tokens: 10000,
200
+ });
201
+
202
+ const calls = (createMock as any).mock.calls;
203
+ assert.strictEqual(calls.length, 1);
204
+ const callArgs = calls[0]?.arguments[0];
205
+ assert.strictEqual(callArgs?.userId, 'user-123');
206
+ assert.strictEqual(callArgs?.tokens, 10000);
207
+ assert.strictEqual((logger.info as any).mock.calls.length, 1);
208
+ });
209
+
210
+ it('should throw error on service error', async () => {
211
+ tokenPackService = createMockTokenPackService({
212
+ create: mock.fn(async () => {
213
+ throw new Error('Create error');
214
+ }),
215
+ });
216
+ service = new TokenService({
217
+ logger,
218
+ tokenPackService,
219
+ paymentService,
220
+ tokenPackPlans,
221
+ });
222
+
223
+ await assert.rejects(
224
+ async () =>
225
+ service.addTokensToUser({
226
+ userId: 'user-123',
227
+ platform: 'telegram',
228
+ packId: 'token-pack-small',
229
+ tokens: 10000,
230
+ }),
231
+ { message: 'Create error' },
232
+ );
233
+ });
234
+ });
235
+
236
+ describe('deductTokens', () => {
237
+ it('should deduct tokens using FIFO from multiple packs', async () => {
238
+ const packs = [
239
+ createMockTokenPackEntity({
240
+ id: 'pack-1',
241
+ tokens: 5000,
242
+ tokensRemaining: 3000,
243
+ purchasedAt: '2025-01-01T00:00:00.000Z',
244
+ }),
245
+ createMockTokenPackEntity({
246
+ id: 'pack-2',
247
+ tokens: 10000,
248
+ tokensRemaining: 10000,
249
+ purchasedAt: '2025-01-15T00:00:00.000Z',
250
+ }),
251
+ ];
252
+ const deductMock = mock.fn(async () => {});
253
+ tokenPackService = createMockTokenPackService({
254
+ getByUser: mock.fn(async () => packs),
255
+ deductTokens: deductMock,
256
+ });
257
+ service = new TokenService({
258
+ logger,
259
+ tokenPackService,
260
+ paymentService,
261
+ tokenPackPlans,
262
+ });
263
+
264
+ await service.deductTokens({
265
+ userId: 'user-123',
266
+ platform: 'telegram',
267
+ amount: 5000, // Deduct 5000 tokens
268
+ });
269
+
270
+ // Should deduct 3000 from pack-1 (oldest) and 2000 from pack-2
271
+ const calls = (deductMock as any).mock.calls;
272
+ assert.strictEqual(calls.length, 2);
273
+ const firstCallArgs = calls[0]?.arguments[0];
274
+ const secondCallArgs = calls[1]?.arguments[0];
275
+ assert.strictEqual(firstCallArgs?.id, 'pack-1');
276
+ assert.strictEqual(firstCallArgs?.amount, 3000);
277
+ assert.strictEqual(secondCallArgs?.id, 'pack-2');
278
+ assert.strictEqual(secondCallArgs?.amount, 2000);
279
+ });
280
+
281
+ it('should deduct from single pack if sufficient', async () => {
282
+ const packs = [
283
+ createMockTokenPackEntity({
284
+ id: 'pack-1',
285
+ tokens: 10000,
286
+ tokensRemaining: 10000,
287
+ }),
288
+ ];
289
+ const deductMock = mock.fn(async () => {});
290
+ tokenPackService = createMockTokenPackService({
291
+ getByUser: mock.fn(async () => packs),
292
+ deductTokens: deductMock,
293
+ });
294
+ service = new TokenService({
295
+ logger,
296
+ tokenPackService,
297
+ paymentService,
298
+ tokenPackPlans,
299
+ });
300
+
301
+ await service.deductTokens({
302
+ userId: 'user-123',
303
+ platform: 'telegram',
304
+ amount: 5000,
305
+ });
306
+
307
+ const calls = (deductMock as any).mock.calls;
308
+ assert.strictEqual(calls.length, 1);
309
+ const callArgs = calls[0]?.arguments[0];
310
+ assert.strictEqual(callArgs?.amount, 5000);
311
+ });
312
+
313
+ it('should throw error if no active packs found', async () => {
314
+ tokenPackService = createMockTokenPackService({
315
+ getByUser: mock.fn(async () => []),
316
+ });
317
+ service = new TokenService({
318
+ logger,
319
+ tokenPackService,
320
+ paymentService,
321
+ tokenPackPlans,
322
+ });
323
+
324
+ await assert.rejects(
325
+ async () =>
326
+ service.deductTokens({
327
+ userId: 'user-123',
328
+ platform: 'telegram',
329
+ amount: 1000,
330
+ }),
331
+ { message: 'No active token packs found for user user-123' },
332
+ );
333
+ });
334
+
335
+ it('should throw error if insufficient tokens', async () => {
336
+ const packs = [
337
+ createMockTokenPackEntity({
338
+ id: 'pack-1',
339
+ tokens: 5000,
340
+ tokensRemaining: 3000,
341
+ }),
342
+ ];
343
+ tokenPackService = createMockTokenPackService({
344
+ getByUser: mock.fn(async () => packs),
345
+ });
346
+ service = new TokenService({
347
+ logger,
348
+ tokenPackService,
349
+ paymentService,
350
+ tokenPackPlans,
351
+ });
352
+
353
+ await assert.rejects(
354
+ async () =>
355
+ service.deductTokens({
356
+ userId: 'user-123',
357
+ platform: 'telegram',
358
+ amount: 5000, // More than available (3000)
359
+ }),
360
+ { message: 'Insufficient tokens. Required: 5000, Available: 3000' },
361
+ );
362
+ });
363
+
364
+ it('should throw error on service error', async () => {
365
+ tokenPackService = createMockTokenPackService({
366
+ getByUser: mock.fn(async () => {
367
+ throw new Error('Fetch error');
368
+ }),
369
+ });
370
+ service = new TokenService({
371
+ logger,
372
+ tokenPackService,
373
+ paymentService,
374
+ tokenPackPlans,
375
+ });
376
+
377
+ await assert.rejects(
378
+ async () =>
379
+ service.deductTokens({
380
+ userId: 'user-123',
381
+ platform: 'telegram',
382
+ amount: 1000,
383
+ }),
384
+ { message: 'Fetch error' },
385
+ );
386
+ });
387
+ });
388
+
389
+ describe('getBalance', () => {
390
+ it('should return total available tokens from all active packs', async () => {
391
+ const packs = [
392
+ createMockTokenPackEntity({
393
+ id: 'pack-1',
394
+ packId: 'token-pack-small',
395
+ tokensRemaining: 3000,
396
+ purchasedAt: '2025-01-01T00:00:00.000Z',
397
+ }),
398
+ createMockTokenPackEntity({
399
+ id: 'pack-2',
400
+ packId: 'token-pack-medium',
401
+ tokensRemaining: 15000,
402
+ purchasedAt: '2025-01-15T00:00:00.000Z',
403
+ }),
404
+ ];
405
+ tokenPackService = createMockTokenPackService({
406
+ getByUser: mock.fn(async () => packs),
407
+ });
408
+ service = new TokenService({
409
+ logger,
410
+ tokenPackService,
411
+ paymentService,
412
+ tokenPackPlans,
413
+ });
414
+
415
+ const result = await service.getBalance({
416
+ userId: 'user-123',
417
+ platform: 'telegram',
418
+ });
419
+
420
+ assert.strictEqual(result.available, 18000); // 3000 + 15000
421
+ assert.strictEqual(result.packs.length, 2);
422
+ assert.strictEqual(result.packs[0].tokensRemaining, 3000);
423
+ assert.strictEqual(result.packs[1].tokensRemaining, 15000);
424
+ });
425
+
426
+ it('should return zero balance when no active packs', async () => {
427
+ tokenPackService = createMockTokenPackService({
428
+ getByUser: mock.fn(async () => []),
429
+ });
430
+ service = new TokenService({
431
+ logger,
432
+ tokenPackService,
433
+ paymentService,
434
+ tokenPackPlans,
435
+ });
436
+
437
+ const result = await service.getBalance({
438
+ userId: 'user-123',
439
+ platform: 'telegram',
440
+ });
441
+
442
+ assert.strictEqual(result.available, 0);
443
+ assert.strictEqual(result.packs.length, 0);
444
+ });
445
+
446
+ it('should return empty balance on error', async () => {
447
+ tokenPackService = createMockTokenPackService({
448
+ getByUser: mock.fn(async () => {
449
+ throw new Error('Fetch error');
450
+ }),
451
+ });
452
+ service = new TokenService({
453
+ logger,
454
+ tokenPackService,
455
+ paymentService,
456
+ tokenPackPlans,
457
+ });
458
+
459
+ const result = await service.getBalance({
460
+ userId: 'user-123',
461
+ platform: 'telegram',
462
+ });
463
+
464
+ assert.strictEqual(result.available, 0);
465
+ assert.strictEqual(result.packs.length, 0);
466
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
467
+ });
468
+
469
+ it('should include pack details in result', async () => {
470
+ const packs = [
471
+ createMockTokenPackEntity({
472
+ id: 'pack-1',
473
+ packId: 'token-pack-small',
474
+ tokensRemaining: 5000,
475
+ purchasedAt: '2025-01-01T00:00:00.000Z',
476
+ }),
477
+ ];
478
+ tokenPackService = createMockTokenPackService({
479
+ getByUser: mock.fn(async () => packs),
480
+ });
481
+ service = new TokenService({
482
+ logger,
483
+ tokenPackService,
484
+ paymentService,
485
+ tokenPackPlans,
486
+ });
487
+
488
+ const result = await service.getBalance({
489
+ userId: 'user-123',
490
+ platform: 'telegram',
491
+ });
492
+
493
+ assert.strictEqual(result.packs[0].id, 'pack-1');
494
+ assert.strictEqual(result.packs[0].packId, 'token-pack-small');
495
+ assert.strictEqual(result.packs[0].tokensRemaining, 5000);
496
+ assert.strictEqual(result.packs[0].purchasedAt, '2025-01-01T00:00:00.000Z');
497
+ });
498
+ });
499
+ });