@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,401 @@
1
+ import { describe, it, beforeEach, mock } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { SubscriptionPlanService } from './service';
4
+ import type { SubscriptionPlanEntity, ISubscriptionPlanRepository } from './types';
5
+ import type { Logger } from '../logger/types';
6
+
7
+ // Mock logger
8
+ const createMockLogger = (): Logger => ({
9
+ info: mock.fn(),
10
+ warning: mock.fn(),
11
+ error: mock.fn(),
12
+ debug: mock.fn(),
13
+ });
14
+
15
+ // Mock subscription plan entity
16
+ const createMockPlan = (overrides: Partial<SubscriptionPlanEntity> = {}): SubscriptionPlanEntity => ({
17
+ id: 'plan-monthly',
18
+ titleCode: 'subscription.plan.monthly.title',
19
+ descriptionCode: 'subscription.plan.monthly.description',
20
+ amount: 100,
21
+ currency: 'UAH',
22
+ features: {
23
+ messages: 1000,
24
+ images: 50,
25
+ voice: 100,
26
+ },
27
+ regularMode: 'monthly',
28
+ count: 1,
29
+ credits: 5000,
30
+ isActive: true,
31
+ createdAt: '2025-01-01T00:00:00.000Z',
32
+ updatedAt: '2025-01-01T00:00:00.000Z',
33
+ ...overrides,
34
+ });
35
+
36
+ // Mock repository
37
+ const createMockRepository = (overrides: Partial<ISubscriptionPlanRepository> = {}): ISubscriptionPlanRepository => ({
38
+ getAll: mock.fn(async () => []),
39
+ getById: mock.fn(async () => null),
40
+ create: mock.fn(async (params) => ({
41
+ id: 'new-plan-id',
42
+ ...params,
43
+ createdAt: new Date().toISOString(),
44
+ updatedAt: new Date().toISOString(),
45
+ })),
46
+ update: mock.fn(async (id, params) => ({
47
+ ...createMockPlan({ id }),
48
+ ...params,
49
+ updatedAt: new Date().toISOString(),
50
+ })),
51
+ deactivate: mock.fn(async () => {}),
52
+ seedDefaults: mock.fn(async () => {}),
53
+ isEmpty: mock.fn(async () => false),
54
+ ...overrides,
55
+ });
56
+
57
+ describe('SubscriptionPlanService', () => {
58
+ let logger: Logger;
59
+ let repository: ISubscriptionPlanRepository;
60
+ let service: SubscriptionPlanService;
61
+
62
+ beforeEach(() => {
63
+ logger = createMockLogger();
64
+ repository = createMockRepository();
65
+ service = new SubscriptionPlanService({ logger, repository });
66
+ });
67
+
68
+ describe('getAll', () => {
69
+ it('should return all plans from repository', async () => {
70
+ const plans = [createMockPlan({ id: 'plan-1' }), createMockPlan({ id: 'plan-2' })];
71
+ repository = createMockRepository({
72
+ getAll: mock.fn(async () => plans),
73
+ isEmpty: mock.fn(async () => false),
74
+ });
75
+ service = new SubscriptionPlanService({ logger, repository });
76
+
77
+ const result = await service.getAll();
78
+
79
+ assert.strictEqual(result.length, 2);
80
+ assert.strictEqual(result[0].id, 'plan-1');
81
+ });
82
+
83
+ it('should return cached plans on second call', async () => {
84
+ const plans = [createMockPlan({ id: 'plan-1' })];
85
+ const getAllMock = mock.fn(async () => plans);
86
+ repository = createMockRepository({
87
+ getAll: getAllMock,
88
+ isEmpty: mock.fn(async () => false),
89
+ });
90
+ service = new SubscriptionPlanService({ logger, repository });
91
+
92
+ await service.getAll();
93
+ await service.getAll();
94
+
95
+ // Should only call repository once due to caching
96
+ assert.strictEqual(getAllMock.mock.calls.length, 1);
97
+ });
98
+
99
+ it('should seed defaults if repository is empty', async () => {
100
+ const seedDefaultsMock = mock.fn(async () => {});
101
+ repository = createMockRepository({
102
+ isEmpty: mock.fn(async () => true),
103
+ seedDefaults: seedDefaultsMock,
104
+ getAll: mock.fn(async () => [createMockPlan()]),
105
+ });
106
+ service = new SubscriptionPlanService({ logger, repository });
107
+
108
+ await service.getAll();
109
+
110
+ assert.strictEqual(seedDefaultsMock.mock.calls.length, 1);
111
+ });
112
+
113
+ it('should return empty array on error', async () => {
114
+ repository = createMockRepository({
115
+ isEmpty: mock.fn(async () => false),
116
+ getAll: mock.fn(async () => {
117
+ throw new Error('DB error');
118
+ }),
119
+ });
120
+ service = new SubscriptionPlanService({ logger, repository });
121
+
122
+ const result = await service.getAll();
123
+
124
+ assert.deepStrictEqual(result, []);
125
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
126
+ });
127
+ });
128
+
129
+ describe('getById', () => {
130
+ it('should return plan from cache if available', async () => {
131
+ const plans = [createMockPlan({ id: 'plan-1' }), createMockPlan({ id: 'plan-2' })];
132
+ const getByIdMock = mock.fn(async () => null);
133
+ repository = createMockRepository({
134
+ getAll: mock.fn(async () => plans),
135
+ getById: getByIdMock,
136
+ isEmpty: mock.fn(async () => false),
137
+ });
138
+ service = new SubscriptionPlanService({ logger, repository });
139
+
140
+ const result = await service.getById('plan-1');
141
+
142
+ assert.strictEqual(result?.id, 'plan-1');
143
+ // getById should not be called if found in cache
144
+ assert.strictEqual(getByIdMock.mock.calls.length, 0);
145
+ });
146
+
147
+ it('should fallback to repository if not in cache', async () => {
148
+ const plan = createMockPlan({ id: 'plan-new' });
149
+ repository = createMockRepository({
150
+ getAll: mock.fn(async () => []),
151
+ getById: mock.fn(async () => plan),
152
+ isEmpty: mock.fn(async () => false),
153
+ });
154
+ service = new SubscriptionPlanService({ logger, repository });
155
+
156
+ const result = await service.getById('plan-new');
157
+
158
+ assert.strictEqual(result?.id, 'plan-new');
159
+ });
160
+
161
+ it('should return null on error', async () => {
162
+ repository = createMockRepository({
163
+ getAll: mock.fn(async () => {
164
+ throw new Error('Cache error');
165
+ }),
166
+ isEmpty: mock.fn(async () => false),
167
+ });
168
+ service = new SubscriptionPlanService({ logger, repository });
169
+
170
+ const result = await service.getById('plan-1');
171
+
172
+ assert.strictEqual(result, null);
173
+ });
174
+ });
175
+
176
+ describe('create', () => {
177
+ it('should create plan successfully', async () => {
178
+ const createMock = mock.fn(async (params) => ({
179
+ ...params,
180
+ id: 'new-plan-id',
181
+ createdAt: new Date().toISOString(),
182
+ updatedAt: new Date().toISOString(),
183
+ }));
184
+ repository = createMockRepository({ create: createMock });
185
+ service = new SubscriptionPlanService({ logger, repository });
186
+
187
+ const params = {
188
+ titleCode: 'new.plan.title',
189
+ descriptionCode: 'new.plan.description',
190
+ amount: 200,
191
+ currency: 'UAH' as const,
192
+ features: { messages: 2000, images: 100, voice: 200 },
193
+ regularMode: 'monthly' as const,
194
+ count: 1,
195
+ credits: 10000,
196
+ isActive: true,
197
+ };
198
+
199
+ const result = await service.create(params);
200
+
201
+ assert.strictEqual(result.id, 'new-plan-id');
202
+ assert.strictEqual(result.titleCode, params.titleCode);
203
+ });
204
+
205
+ it('should invalidate cache after create', async () => {
206
+ const plans = [createMockPlan({ id: 'plan-1' })];
207
+ const getAllMock = mock.fn(async () => plans);
208
+ repository = createMockRepository({
209
+ getAll: getAllMock,
210
+ isEmpty: mock.fn(async () => false),
211
+ });
212
+ service = new SubscriptionPlanService({ logger, repository });
213
+
214
+ await service.getAll(); // Populate cache
215
+ await service.create({
216
+ titleCode: 'new.plan.title',
217
+ descriptionCode: 'new.plan.description',
218
+ amount: 200,
219
+ currency: 'UAH',
220
+ features: { messages: 2000, images: 100, voice: 200 },
221
+ regularMode: 'monthly',
222
+ count: 1,
223
+ credits: 10000,
224
+ isActive: true,
225
+ });
226
+ await service.getAll(); // Should hit repository again
227
+
228
+ assert.strictEqual(getAllMock.mock.calls.length, 2);
229
+ });
230
+
231
+ it('should throw error on repository error', async () => {
232
+ repository = createMockRepository({
233
+ create: mock.fn(async () => {
234
+ throw new Error('Create error');
235
+ }),
236
+ });
237
+ service = new SubscriptionPlanService({ logger, repository });
238
+
239
+ await assert.rejects(
240
+ async () =>
241
+ service.create({
242
+ titleCode: 'new.plan.title',
243
+ descriptionCode: 'new.plan.description',
244
+ amount: 200,
245
+ currency: 'UAH',
246
+ features: { messages: 2000, images: 100, voice: 200 },
247
+ regularMode: 'monthly',
248
+ count: 1,
249
+ credits: 10000,
250
+ isActive: true,
251
+ }),
252
+ { message: 'Create error' },
253
+ );
254
+ });
255
+ });
256
+
257
+ describe('update', () => {
258
+ it('should update plan successfully', async () => {
259
+ const updateMock = mock.fn(async (id, params) => ({
260
+ ...createMockPlan({ id }),
261
+ ...params,
262
+ updatedAt: new Date().toISOString(),
263
+ }));
264
+ repository = createMockRepository({ update: updateMock });
265
+ service = new SubscriptionPlanService({ logger, repository });
266
+
267
+ const result = await service.update('plan-1', { amount: 150 });
268
+
269
+ assert.strictEqual(result.id, 'plan-1');
270
+ assert.strictEqual(result.amount, 150);
271
+ });
272
+
273
+ it('should invalidate cache after update', async () => {
274
+ const plans = [createMockPlan({ id: 'plan-1' })];
275
+ const getAllMock = mock.fn(async () => plans);
276
+ repository = createMockRepository({
277
+ getAll: getAllMock,
278
+ isEmpty: mock.fn(async () => false),
279
+ });
280
+ service = new SubscriptionPlanService({ logger, repository });
281
+
282
+ await service.getAll(); // Populate cache
283
+ await service.update('plan-1', { amount: 150 });
284
+ await service.getAll(); // Should hit repository again
285
+
286
+ assert.strictEqual(getAllMock.mock.calls.length, 2);
287
+ });
288
+
289
+ it('should throw error on repository error', async () => {
290
+ repository = createMockRepository({
291
+ update: mock.fn(async () => {
292
+ throw new Error('Update error');
293
+ }),
294
+ });
295
+ service = new SubscriptionPlanService({ logger, repository });
296
+
297
+ await assert.rejects(async () => service.update('plan-1', { amount: 150 }), { message: 'Update error' });
298
+ });
299
+ });
300
+
301
+ describe('deactivate', () => {
302
+ it('should deactivate plan successfully', async () => {
303
+ const deactivateMock = mock.fn(async () => {});
304
+ repository = createMockRepository({ deactivate: deactivateMock });
305
+ service = new SubscriptionPlanService({ logger, repository });
306
+
307
+ await service.deactivate('plan-1');
308
+
309
+ const calls = (deactivateMock as any).mock.calls;
310
+ assert.strictEqual(calls.length, 1);
311
+ const callArg = calls[0]?.arguments[0];
312
+ assert.strictEqual(callArg, 'plan-1');
313
+ });
314
+
315
+ it('should invalidate cache after deactivate', async () => {
316
+ const plans = [createMockPlan({ id: 'plan-1' })];
317
+ const getAllMock = mock.fn(async () => plans);
318
+ repository = createMockRepository({
319
+ getAll: getAllMock,
320
+ isEmpty: mock.fn(async () => false),
321
+ });
322
+ service = new SubscriptionPlanService({ logger, repository });
323
+
324
+ await service.getAll(); // Populate cache
325
+ await service.deactivate('plan-1');
326
+ await service.getAll(); // Should hit repository again
327
+
328
+ assert.strictEqual(getAllMock.mock.calls.length, 2);
329
+ });
330
+
331
+ it('should throw error on repository error', async () => {
332
+ repository = createMockRepository({
333
+ deactivate: mock.fn(async () => {
334
+ throw new Error('Deactivate error');
335
+ }),
336
+ });
337
+ service = new SubscriptionPlanService({ logger, repository });
338
+
339
+ await assert.rejects(async () => service.deactivate('plan-1'), { message: 'Deactivate error' });
340
+ });
341
+ });
342
+
343
+ describe('resetCache', () => {
344
+ it('should clear the cache', async () => {
345
+ const plans = [createMockPlan({ id: 'plan-1' })];
346
+ const getAllMock = mock.fn(async () => plans);
347
+ repository = createMockRepository({
348
+ getAll: getAllMock,
349
+ isEmpty: mock.fn(async () => false),
350
+ });
351
+ service = new SubscriptionPlanService({ logger, repository });
352
+
353
+ await service.getAll(); // Populate cache
354
+ service.resetCache();
355
+ await service.getAll(); // Should hit repository again
356
+
357
+ assert.strictEqual(getAllMock.mock.calls.length, 2);
358
+ assert.strictEqual((logger.info as any).mock.calls.length, 1);
359
+ });
360
+ });
361
+
362
+ describe('seedDefaults', () => {
363
+ it('should seed defaults if repository is empty', async () => {
364
+ const seedDefaultsMock = mock.fn(async () => {});
365
+ repository = createMockRepository({
366
+ isEmpty: mock.fn(async () => true),
367
+ seedDefaults: seedDefaultsMock,
368
+ });
369
+ service = new SubscriptionPlanService({ logger, repository });
370
+
371
+ await service.seedDefaults();
372
+
373
+ assert.strictEqual(seedDefaultsMock.mock.calls.length, 1);
374
+ assert.strictEqual((logger.info as any).mock.calls.length, 1);
375
+ });
376
+
377
+ it('should not seed if repository is not empty', async () => {
378
+ const seedDefaultsMock = mock.fn(async () => {});
379
+ repository = createMockRepository({
380
+ isEmpty: mock.fn(async () => false),
381
+ seedDefaults: seedDefaultsMock,
382
+ });
383
+ service = new SubscriptionPlanService({ logger, repository });
384
+
385
+ await service.seedDefaults();
386
+
387
+ assert.strictEqual(seedDefaultsMock.mock.calls.length, 0);
388
+ });
389
+
390
+ it('should throw error on repository error', async () => {
391
+ repository = createMockRepository({
392
+ isEmpty: mock.fn(async () => {
393
+ throw new Error('Empty check error');
394
+ }),
395
+ });
396
+ service = new SubscriptionPlanService({ logger, repository });
397
+
398
+ await assert.rejects(async () => service.seedDefaults(), { message: 'Empty check error' });
399
+ });
400
+ });
401
+ });
@@ -0,0 +1,148 @@
1
+ import { Logger } from '../logger/types';
2
+ import { InMemoryCache } from '../cache/InMemoryCache';
3
+ import { SubscriptionPlanRepository } from './repository';
4
+ import { SubscriptionPlanEntity, ISubscriptionPlanRepository, ISubscriptionPlanService } from './types';
5
+ import { CACHE_TTL_MS, DEFAULT_SUBSCRIPTION_PLANS } from './const';
6
+
7
+ const CACHE_KEY_ALL_PLANS = 'all_plans';
8
+
9
+ export class SubscriptionPlanService implements ISubscriptionPlanService {
10
+ private readonly logger: Logger;
11
+ private readonly repository: ISubscriptionPlanRepository;
12
+ private readonly cache: InMemoryCache<SubscriptionPlanEntity[]>;
13
+
14
+ constructor({
15
+ logger,
16
+ repository,
17
+ }: {
18
+ logger: Logger;
19
+ repository?: ISubscriptionPlanRepository;
20
+ }) {
21
+ this.logger = logger;
22
+ this.repository = repository || new SubscriptionPlanRepository();
23
+ this.cache = new InMemoryCache<SubscriptionPlanEntity[]>({ ttl: CACHE_TTL_MS });
24
+ }
25
+
26
+ public async getAll(): Promise<SubscriptionPlanEntity[]> {
27
+ try {
28
+ const cached = this.cache.get(CACHE_KEY_ALL_PLANS);
29
+ if (cached) {
30
+ return cached;
31
+ }
32
+
33
+ await this.ensureSeeded();
34
+
35
+ const plans = await this.repository.getAll();
36
+ this.cache.set(CACHE_KEY_ALL_PLANS, plans);
37
+ return plans;
38
+ } catch (error) {
39
+ this.logger.error({
40
+ message: 'Error in subscription plan service getAll',
41
+ payload: { error: JSON.stringify(error) },
42
+ });
43
+ return [];
44
+ }
45
+ }
46
+
47
+ public async getById(id: string): Promise<SubscriptionPlanEntity | null> {
48
+ try {
49
+ const allPlans = await this.getAll();
50
+ const cachedPlan = allPlans.find((plan) => plan.id === id);
51
+ if (cachedPlan) {
52
+ return cachedPlan;
53
+ }
54
+
55
+ return await this.repository.getById(id);
56
+ } catch (error) {
57
+ this.logger.error({
58
+ message: 'Error in subscription plan service getById',
59
+ payload: { id, error: JSON.stringify(error) },
60
+ });
61
+ return null;
62
+ }
63
+ }
64
+
65
+ public async create(
66
+ params: Omit<SubscriptionPlanEntity, 'id' | 'createdAt' | 'updatedAt'>,
67
+ ): Promise<SubscriptionPlanEntity> {
68
+ try {
69
+ const plan = await this.repository.create(params);
70
+ this.invalidateCache();
71
+ return plan;
72
+ } catch (error) {
73
+ this.logger.error({
74
+ message: 'Error in subscription plan service create',
75
+ payload: { params: JSON.stringify(params), error: JSON.stringify(error) },
76
+ });
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ public async update(
82
+ id: string,
83
+ params: Partial<Omit<SubscriptionPlanEntity, 'id' | 'createdAt' | 'updatedAt'>>,
84
+ ): Promise<SubscriptionPlanEntity> {
85
+ try {
86
+ const plan = await this.repository.update(id, params);
87
+ this.invalidateCache();
88
+ return plan;
89
+ } catch (error) {
90
+ this.logger.error({
91
+ message: 'Error in subscription plan service update',
92
+ payload: { id, params: JSON.stringify(params), error: JSON.stringify(error) },
93
+ });
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ public async deactivate(id: string): Promise<void> {
99
+ try {
100
+ await this.repository.deactivate(id);
101
+ this.invalidateCache();
102
+ } catch (error) {
103
+ this.logger.error({
104
+ message: 'Error in subscription plan service deactivate',
105
+ payload: { id, error: JSON.stringify(error) },
106
+ });
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ public resetCache(): void {
112
+ this.cache.clear();
113
+ this.logger.info({
114
+ message: 'Subscription plan cache reset',
115
+ });
116
+ }
117
+
118
+ public async seedDefaults(): Promise<void> {
119
+ try {
120
+ const isEmpty = await this.repository.isEmpty();
121
+ if (isEmpty) {
122
+ await this.repository.seedDefaults(DEFAULT_SUBSCRIPTION_PLANS);
123
+ this.invalidateCache();
124
+ this.logger.info({
125
+ message: 'Default subscription plans seeded',
126
+ payload: { count: DEFAULT_SUBSCRIPTION_PLANS.length },
127
+ });
128
+ }
129
+ } catch (error) {
130
+ this.logger.error({
131
+ message: 'Error in subscription plan service seedDefaults',
132
+ payload: { error: JSON.stringify(error) },
133
+ });
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ private async ensureSeeded(): Promise<void> {
139
+ const isEmpty = await this.repository.isEmpty();
140
+ if (isEmpty) {
141
+ await this.seedDefaults();
142
+ }
143
+ }
144
+
145
+ private invalidateCache(): void {
146
+ this.cache.delete(CACHE_KEY_ALL_PLANS);
147
+ }
148
+ }
@@ -0,0 +1,67 @@
1
+ import { NestedPathsAccess } from '../../types/utilities';
2
+
3
+ type SubscriptionPlanEntity = {
4
+ id: string;
5
+ titleCode: string;
6
+ descriptionCode: string;
7
+ amount: number;
8
+ currency: 'UAH' | 'USD';
9
+ features: {
10
+ messages: number;
11
+ images: number;
12
+ voice: number;
13
+ };
14
+ regularMode: 'daily' | 'monthly' | 'yearly';
15
+ count: number;
16
+ credits: number;
17
+ isActive: boolean;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ };
21
+
22
+ type SubscriptionPlanFieldPath = NestedPathsAccess<SubscriptionPlanEntity>;
23
+
24
+ interface ISubscriptionPlanRepository {
25
+ getAll(): Promise<SubscriptionPlanEntity[]>;
26
+
27
+ getById(id: string): Promise<SubscriptionPlanEntity | null>;
28
+
29
+ create(params: Omit<SubscriptionPlanEntity, 'id' | 'createdAt' | 'updatedAt'>): Promise<SubscriptionPlanEntity>;
30
+
31
+ update(
32
+ id: string,
33
+ params: Partial<Omit<SubscriptionPlanEntity, 'id' | 'createdAt' | 'updatedAt'>>,
34
+ ): Promise<SubscriptionPlanEntity>;
35
+
36
+ deactivate(id: string): Promise<void>;
37
+
38
+ seedDefaults(defaultPlans: Omit<SubscriptionPlanEntity, 'createdAt' | 'updatedAt'>[]): Promise<void>;
39
+
40
+ isEmpty(): Promise<boolean>;
41
+ }
42
+
43
+ interface ISubscriptionPlanService {
44
+ getAll(): Promise<SubscriptionPlanEntity[]>;
45
+
46
+ getById(id: string): Promise<SubscriptionPlanEntity | null>;
47
+
48
+ create(params: Omit<SubscriptionPlanEntity, 'id' | 'createdAt' | 'updatedAt'>): Promise<SubscriptionPlanEntity>;
49
+
50
+ update(
51
+ id: string,
52
+ params: Partial<Omit<SubscriptionPlanEntity, 'id' | 'createdAt' | 'updatedAt'>>,
53
+ ): Promise<SubscriptionPlanEntity>;
54
+
55
+ deactivate(id: string): Promise<void>;
56
+
57
+ resetCache(): void;
58
+
59
+ seedDefaults(): Promise<void>;
60
+ }
61
+
62
+ export type {
63
+ SubscriptionPlanEntity,
64
+ SubscriptionPlanFieldPath,
65
+ ISubscriptionPlanRepository,
66
+ ISubscriptionPlanService,
67
+ };
@@ -0,0 +1,64 @@
1
+ import { TokenPackPlan } from './types';
2
+
3
+ /**
4
+ * Default token pack plans available for purchase.
5
+ * Fixed packs: 10000, 30000, 75000 tokens
6
+ */
7
+ export const DEFAULT_TOKEN_PACK_PLANS: TokenPackPlan[] = [
8
+ // UAH Plans
9
+ {
10
+ id: 'token-pack-small',
11
+ titleCode: 'token.pack.small.title',
12
+ descriptionCode: 'token.pack.small.description',
13
+ tokens: 10000,
14
+ amount: 49,
15
+ currency: 'UAH',
16
+ isActive: true,
17
+ },
18
+ {
19
+ id: 'token-pack-medium',
20
+ titleCode: 'token.pack.medium.title',
21
+ descriptionCode: 'token.pack.medium.description',
22
+ tokens: 30000,
23
+ amount: 129,
24
+ currency: 'UAH',
25
+ isActive: true,
26
+ },
27
+ {
28
+ id: 'token-pack-large',
29
+ titleCode: 'token.pack.large.title',
30
+ descriptionCode: 'token.pack.large.description',
31
+ tokens: 75000,
32
+ amount: 299,
33
+ currency: 'UAH',
34
+ isActive: true,
35
+ },
36
+ // USD Plans
37
+ {
38
+ id: 'token-pack-small-usd',
39
+ titleCode: 'token.pack.small.title',
40
+ descriptionCode: 'token.pack.small.description',
41
+ tokens: 10000,
42
+ amount: 2,
43
+ currency: 'USD',
44
+ isActive: true,
45
+ },
46
+ {
47
+ id: 'token-pack-medium-usd',
48
+ titleCode: 'token.pack.medium.title',
49
+ descriptionCode: 'token.pack.medium.description',
50
+ tokens: 30000,
51
+ amount: 5,
52
+ currency: 'USD',
53
+ isActive: true,
54
+ },
55
+ {
56
+ id: 'token-pack-large-usd',
57
+ titleCode: 'token.pack.large.title',
58
+ descriptionCode: 'token.pack.large.description',
59
+ tokens: 75000,
60
+ amount: 10,
61
+ currency: 'USD',
62
+ isActive: true,
63
+ },
64
+ ];
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './service';
3
+ export * from './const';