@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,14 @@
1
+ export * from './repository';
2
+ export * from './service';
3
+ export * from './types';
4
+ export * from './const';
5
+ export * from './api';
6
+ export * from './utils';
7
+ export * from './wayforpay.service';
8
+ export * from './wayforpay.types';
9
+ export * from './webhook.service';
10
+ export * from './webhook.types';
11
+ export * from './webhook.handler';
12
+ export * from './subscription-check-webhook.service';
13
+ export * from './subscription-check-webhook.types';
14
+ export * from './subscription-check-webhook.handler';
@@ -0,0 +1,125 @@
1
+ import { FieldValue, Firestore, getFirestore, QueryDocumentSnapshot } from 'firebase-admin/firestore';
2
+ import { PaymentEntity, PaymentFieldPath, IPaymentRepository } from './types';
3
+ import { DEFAULT_PAYMENT_ENTITY } from './const';
4
+
5
+ export type UpdateDBPaymentFields = [PaymentFieldPath, FieldValue | string | number | boolean | Date | [] | {}][];
6
+
7
+ export class PaymentRepository implements IPaymentRepository {
8
+ private readonly db: Firestore;
9
+ private readonly collectionName = 'payments';
10
+
11
+ constructor({ db }: { db?: Firestore } = {}) {
12
+ this.db = db || getFirestore();
13
+ }
14
+
15
+ public async getByOrderReference(orderReference: string): Promise<PaymentEntity | null> {
16
+ const querySnapshot = await this.db
17
+ .collection(this.collectionName)
18
+ .where('orderReference', '==', orderReference)
19
+ .limit(1)
20
+ .get();
21
+
22
+ if (querySnapshot.empty) {
23
+ return null;
24
+ }
25
+
26
+ const doc = querySnapshot.docs[0];
27
+ return this.mapDocumentToEntity(doc);
28
+ }
29
+
30
+ public async getByUser(params: {
31
+ userId: string;
32
+ platform: string;
33
+ status?: PaymentEntity['status'];
34
+ }): Promise<PaymentEntity[]> {
35
+ const { userId, platform, status } = params;
36
+
37
+ let query = this.db
38
+ .collection(this.collectionName)
39
+ .where('platform', '==', platform)
40
+ .where('userId', '==', userId);
41
+
42
+ if (status) {
43
+ query = query.where('status', '==', status);
44
+ }
45
+
46
+ const querySnapshot = await query.get();
47
+
48
+ if (querySnapshot.empty) {
49
+ return [];
50
+ }
51
+
52
+ return querySnapshot.docs.map((doc) => this.mapDocumentToEntity(doc));
53
+ }
54
+
55
+ public async create(paymentData: Omit<PaymentEntity, 'id'>): Promise<PaymentEntity> {
56
+ const docRef = this.db.collection(this.collectionName).doc();
57
+
58
+ const paymentEntity = {
59
+ id: docRef.id,
60
+ ...DEFAULT_PAYMENT_ENTITY,
61
+ ...paymentData,
62
+ };
63
+
64
+ docRef.set(paymentEntity);
65
+
66
+ return paymentEntity;
67
+ }
68
+
69
+ public async updateFields({ fields, id }: { id: string; fields: UpdateDBPaymentFields }): Promise<void> {
70
+ const updateObject = this.buildUpdateObject(fields);
71
+
72
+ const querySnapshot = await this.db.collection(this.collectionName).doc(id).get();
73
+
74
+ if (!querySnapshot.exists) {
75
+ throw new Error(`Payment not found for id: ${id}`);
76
+ }
77
+
78
+ await querySnapshot.ref.update(updateObject);
79
+ }
80
+
81
+ public async getExpiredPendingPayments(
82
+ params: {
83
+ hoursOld?: number;
84
+ } = {},
85
+ ): Promise<PaymentEntity[]> {
86
+ const { hoursOld = 24 } = params;
87
+
88
+ const cutoffTime = this.calculateCutoffTime(hoursOld);
89
+
90
+ const querySnapshot = await this.db
91
+ .collection(this.collectionName)
92
+ .where('status', '==', 'pending')
93
+ .where('createdAt', '<', cutoffTime.toISOString())
94
+ .get();
95
+
96
+ if (querySnapshot.empty) {
97
+ return [];
98
+ }
99
+
100
+ return querySnapshot.docs.map((doc) => this.mapDocumentToEntity(doc));
101
+ }
102
+
103
+ private mapDocumentToEntity(doc: QueryDocumentSnapshot): PaymentEntity {
104
+ return {
105
+ id: doc.id,
106
+ ...doc.data(),
107
+ } as PaymentEntity;
108
+ }
109
+
110
+ private buildUpdateObject(fields: UpdateDBPaymentFields): Record<string, any> {
111
+ const updateObject: Record<string, any> = {};
112
+
113
+ fields.forEach(([fieldPath, value]) => {
114
+ updateObject[fieldPath] = value;
115
+ });
116
+
117
+ return updateObject;
118
+ }
119
+
120
+ private calculateCutoffTime(hoursOld: number): Date {
121
+ const cutoffTime = new Date();
122
+ cutoffTime.setHours(cutoffTime.getHours() - hoursOld);
123
+ return cutoffTime;
124
+ }
125
+ }
@@ -0,0 +1,400 @@
1
+ import { describe, it, beforeEach, mock } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { PaymentService } from './service';
4
+ import type { PaymentEntity, IPaymentRepository } from './types';
5
+ import type { IWayForPayService, WayForPayCallbackData } from './wayforpay.types';
6
+ import type { Logger } from '../logger/types';
7
+
8
+ // Mock logger
9
+ const createMockLogger = (): Logger => ({
10
+ info: mock.fn(),
11
+ warning: mock.fn(),
12
+ error: mock.fn(),
13
+ debug: mock.fn(),
14
+ });
15
+
16
+ // Mock payment entity
17
+ const createMockPayment = (overrides: Partial<PaymentEntity> = {}): PaymentEntity => ({
18
+ id: 'pay-123',
19
+ orderReference: 'order-123',
20
+ userId: 'user-123',
21
+ platform: 'telegram',
22
+ status: 'pending',
23
+ paymentLink: 'https://pay.example.com/link',
24
+ planId: 'plan-monthly',
25
+ paymentType: 'subscription',
26
+ amount: 100,
27
+ currency: 'UAH',
28
+ createdAt: new Date().toISOString(),
29
+ provider: 'wayforpay',
30
+ ...overrides,
31
+ });
32
+
33
+ // Mock callback data
34
+ const createMockCallbackData = (overrides: Partial<WayForPayCallbackData> = {}): WayForPayCallbackData =>
35
+ ({
36
+ merchantAccount: 'test_merchant',
37
+ orderReference: 'order-123',
38
+ merchantSignature: 'valid-signature',
39
+ amount: 100,
40
+ currency: 'UAH',
41
+ authCode: 'auth-123',
42
+ email: 'test@example.com',
43
+ phone: '+380123456789',
44
+ createdDate: Date.now(),
45
+ processingDate: Date.now(),
46
+ cardPan: '4111****1111',
47
+ cardType: 'VISA',
48
+ issuerBankCountry: 'UA',
49
+ issuerBankName: 'Test Bank',
50
+ recToken: 'rec-token-123',
51
+ transactionStatus: 'Approved',
52
+ reason: 'Ok',
53
+ reasonCode: 1100,
54
+ fee: 2.5,
55
+ paymentSystem: 'VISA',
56
+ ...overrides,
57
+ }) as WayForPayCallbackData;
58
+
59
+ // Mock repository
60
+ const createMockRepository = (overrides: Partial<IPaymentRepository> = {}): IPaymentRepository => ({
61
+ getByOrderReference: mock.fn(async () => null),
62
+ getByUser: mock.fn(async () => []),
63
+ create: mock.fn(async (data) => ({ id: 'pay-new', ...data })),
64
+ updateFields: mock.fn(async () => {}),
65
+ getExpiredPendingPayments: mock.fn(async () => []),
66
+ ...overrides,
67
+ });
68
+
69
+ // Mock WayForPay service
70
+ const createMockWayForPayService = (overrides: Partial<IWayForPayService> = {}): IWayForPayService => ({
71
+ generateSubscriptionPaymentUrl: mock.fn(async () => ({
72
+ url: 'https://pay.wayforpay.com/invoice',
73
+ orderReference: 'order-new',
74
+ })),
75
+ generateOneTimePaymentUrl: mock.fn(async () => ({
76
+ url: 'https://pay.wayforpay.com/invoice',
77
+ orderReference: 'order-new',
78
+ })),
79
+ verifyCallbackSignature: mock.fn(() => ({ isValid: true })),
80
+ buildWebhookResponse: mock.fn(() => ({
81
+ orderReference: 'order-123',
82
+ status: 'accept' as const,
83
+ time: Date.now(),
84
+ signature: 'response-signature',
85
+ })),
86
+ ...overrides,
87
+ });
88
+
89
+ describe('PaymentService', () => {
90
+ let logger: Logger;
91
+ let repository: IPaymentRepository;
92
+ let wayForPayService: IWayForPayService;
93
+ let service: PaymentService;
94
+
95
+ beforeEach(() => {
96
+ logger = createMockLogger();
97
+ repository = createMockRepository();
98
+ wayForPayService = createMockWayForPayService();
99
+ service = new PaymentService({ logger, repository, wayForPayService });
100
+ });
101
+
102
+ describe('getByOrderReference', () => {
103
+ it('should return payment when found', async () => {
104
+ const mockPayment = createMockPayment();
105
+ repository = createMockRepository({
106
+ getByOrderReference: mock.fn(async () => mockPayment),
107
+ });
108
+ service = new PaymentService({ logger, repository, wayForPayService });
109
+
110
+ const result = await service.getByOrderReference('order-123');
111
+
112
+ assert.deepStrictEqual(result, mockPayment);
113
+ });
114
+
115
+ it('should return null when payment not found', async () => {
116
+ const result = await service.getByOrderReference('non-existent');
117
+
118
+ assert.strictEqual(result, null);
119
+ });
120
+
121
+ it('should return null and log error on repository error', async () => {
122
+ repository = createMockRepository({
123
+ getByOrderReference: mock.fn(async () => {
124
+ throw new Error('DB error');
125
+ }),
126
+ });
127
+ service = new PaymentService({ logger, repository, wayForPayService });
128
+
129
+ const result = await service.getByOrderReference('order-123');
130
+
131
+ assert.strictEqual(result, null);
132
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
133
+ });
134
+ });
135
+
136
+ describe('getByUser', () => {
137
+ it('should return payments for user', async () => {
138
+ const payments = [createMockPayment(), createMockPayment({ id: 'pay-456' })];
139
+ repository = createMockRepository({
140
+ getByUser: mock.fn(async () => payments),
141
+ });
142
+ service = new PaymentService({ logger, repository, wayForPayService });
143
+
144
+ const result = await service.getByUser({
145
+ userId: 'user-123',
146
+ platform: 'telegram',
147
+ });
148
+
149
+ assert.strictEqual(result.length, 2);
150
+ });
151
+
152
+ it('should filter by status when provided', async () => {
153
+ const getByUserMock = mock.fn(async () => [createMockPayment({ status: 'pending' })]);
154
+ repository = createMockRepository({ getByUser: getByUserMock });
155
+ service = new PaymentService({ logger, repository, wayForPayService });
156
+
157
+ await service.getByUser({
158
+ userId: 'user-123',
159
+ platform: 'telegram',
160
+ status: 'pending',
161
+ });
162
+
163
+ const calls = (getByUserMock as any).mock.calls;
164
+ const callArgs = calls[0]?.arguments[0];
165
+ assert.strictEqual(callArgs?.userId, 'user-123');
166
+ assert.strictEqual(callArgs?.platform, 'telegram');
167
+ assert.strictEqual(callArgs?.status, 'pending');
168
+ });
169
+
170
+ it('should return empty array on error', async () => {
171
+ repository = createMockRepository({
172
+ getByUser: mock.fn(async () => {
173
+ throw new Error('DB error');
174
+ }),
175
+ });
176
+ service = new PaymentService({ logger, repository, wayForPayService });
177
+
178
+ const result = await service.getByUser({
179
+ userId: 'user-123',
180
+ platform: 'telegram',
181
+ });
182
+
183
+ assert.deepStrictEqual(result, []);
184
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
185
+ });
186
+ });
187
+
188
+ describe('create', () => {
189
+ it('should create payment successfully', async () => {
190
+ const paymentData = {
191
+ orderReference: 'order-new',
192
+ userId: 'user-123',
193
+ status: 'pending' as const,
194
+ paymentLink: 'https://pay.example.com/new',
195
+ planId: 'plan-monthly',
196
+ paymentType: 'subscription' as const,
197
+ amount: 100,
198
+ currency: 'UAH' as const,
199
+ createdAt: new Date().toISOString(),
200
+ platform: 'telegram',
201
+ provider: 'wayforpay' as const,
202
+ };
203
+
204
+ const result = await service.create(paymentData);
205
+
206
+ assert.strictEqual(result.orderReference, paymentData.orderReference);
207
+ assert.strictEqual(result.userId, paymentData.userId);
208
+ });
209
+
210
+ it('should throw error on repository error', async () => {
211
+ repository = createMockRepository({
212
+ create: mock.fn(async () => {
213
+ throw new Error('Create error');
214
+ }),
215
+ });
216
+ service = new PaymentService({ logger, repository, wayForPayService });
217
+
218
+ await assert.rejects(
219
+ async () =>
220
+ service.create({
221
+ orderReference: 'order-new',
222
+ userId: 'user-123',
223
+ status: 'pending',
224
+ paymentLink: 'https://pay.example.com/new',
225
+ planId: 'plan-monthly',
226
+ paymentType: 'subscription',
227
+ amount: 100,
228
+ currency: 'UAH',
229
+ createdAt: new Date().toISOString(),
230
+ platform: 'telegram',
231
+ provider: 'wayforpay',
232
+ }),
233
+ { message: 'Create error' },
234
+ );
235
+ });
236
+ });
237
+
238
+ describe('updateStatus', () => {
239
+ it('should update status successfully', async () => {
240
+ const updateFieldsMock = mock.fn(async () => {});
241
+ repository = createMockRepository({ updateFields: updateFieldsMock });
242
+ service = new PaymentService({ logger, repository, wayForPayService });
243
+
244
+ await service.updateStatus({ id: 'pay-123', status: 'completed' });
245
+
246
+ const calls = (updateFieldsMock as any).mock.calls;
247
+ assert.strictEqual(calls.length, 1);
248
+ const callArgs = calls[0]?.arguments[0];
249
+ assert.strictEqual(callArgs?.id, 'pay-123');
250
+ assert.deepStrictEqual(callArgs?.fields, [['status', 'completed']]);
251
+ });
252
+
253
+ it('should throw error on repository error', async () => {
254
+ repository = createMockRepository({
255
+ updateFields: mock.fn(async () => {
256
+ throw new Error('Update error');
257
+ }),
258
+ });
259
+ service = new PaymentService({ logger, repository, wayForPayService });
260
+
261
+ await assert.rejects(async () => service.updateStatus({ id: 'pay-123', status: 'completed' }), {
262
+ message: 'Update error',
263
+ });
264
+ });
265
+ });
266
+
267
+ describe('changeStatus', () => {
268
+ it('should call updateStatus', async () => {
269
+ const updateFieldsMock = mock.fn(async () => {});
270
+ repository = createMockRepository({ updateFields: updateFieldsMock });
271
+ service = new PaymentService({ logger, repository, wayForPayService });
272
+
273
+ await service.changeStatus({ id: 'pay-123', status: 'failed' });
274
+
275
+ assert.strictEqual(updateFieldsMock.mock.calls.length, 1);
276
+ });
277
+ });
278
+
279
+ describe('getExpiredPendingPayments', () => {
280
+ it('should return expired pending payments', async () => {
281
+ const expiredPayments = [
282
+ createMockPayment({ status: 'pending', createdAt: '2025-01-01T00:00:00.000Z' }),
283
+ ];
284
+ repository = createMockRepository({
285
+ getExpiredPendingPayments: mock.fn(async () => expiredPayments),
286
+ });
287
+ service = new PaymentService({ logger, repository, wayForPayService });
288
+
289
+ const result = await service.getExpiredPendingPayments({ hoursOld: 24 });
290
+
291
+ assert.strictEqual(result.length, 1);
292
+ });
293
+
294
+ it('should return empty array on error', async () => {
295
+ repository = createMockRepository({
296
+ getExpiredPendingPayments: mock.fn(async () => {
297
+ throw new Error('DB error');
298
+ }),
299
+ });
300
+ service = new PaymentService({ logger, repository, wayForPayService });
301
+
302
+ const result = await service.getExpiredPendingPayments();
303
+
304
+ assert.deepStrictEqual(result, []);
305
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
306
+ });
307
+ });
308
+
309
+ describe('createPaymentIntent', () => {
310
+ // Note: These tests require the external WayForPay API which uses createPaymentAPI
311
+ // directly. In a test environment without valid credentials, the API will fail.
312
+ // These tests verify the error handling behavior.
313
+
314
+ it('should throw and log error when API fails due to missing credentials', async () => {
315
+ // Without proper WayForPay credentials, the API will fail
316
+ await assert.rejects(
317
+ async () =>
318
+ service.createPaymentIntent({
319
+ userId: 'user-123',
320
+ platform: 'telegram',
321
+ planId: 'plan-monthly',
322
+ productName: 'Monthly Subscription',
323
+ productPrice: 100,
324
+ currency: 'UAH',
325
+ regularCount: 3,
326
+ regularMode: 'monthly',
327
+ language: 'UA',
328
+ }),
329
+ );
330
+ // Error should be logged
331
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
332
+ });
333
+ });
334
+
335
+ describe('createTokenPackPaymentIntent', () => {
336
+ it('should throw and log error when API fails due to missing credentials', async () => {
337
+ // Without proper WayForPay credentials, the API will fail
338
+ await assert.rejects(
339
+ async () =>
340
+ service.createTokenPackPaymentIntent({
341
+ userId: 'user-123',
342
+ platform: 'telegram',
343
+ packId: 'token-pack-small',
344
+ productName: 'Token Pack - 10000 tokens',
345
+ productPrice: 49,
346
+ currency: 'UAH',
347
+ language: 'UA',
348
+ }),
349
+ );
350
+ // Error should be logged
351
+ assert.strictEqual((logger.error as any).mock.calls.length, 1);
352
+ });
353
+ });
354
+
355
+ describe('validateSignature', () => {
356
+ it('should return valid result for valid signature', () => {
357
+ wayForPayService = createMockWayForPayService({
358
+ verifyCallbackSignature: mock.fn(() => ({ isValid: true })),
359
+ });
360
+ service = new PaymentService({ logger, repository, wayForPayService });
361
+
362
+ const callbackData = createMockCallbackData();
363
+ const result = service.validateSignature(callbackData);
364
+
365
+ assert.strictEqual(result.isValid, true);
366
+ assert.strictEqual(result.error, undefined);
367
+ });
368
+
369
+ it('should return invalid result for invalid signature', () => {
370
+ wayForPayService = createMockWayForPayService({
371
+ verifyCallbackSignature: mock.fn(() => ({
372
+ isValid: false,
373
+ error: 'Signature mismatch',
374
+ })),
375
+ });
376
+ service = new PaymentService({ logger, repository, wayForPayService });
377
+
378
+ const callbackData = createMockCallbackData({ merchantSignature: 'invalid' });
379
+ const result = service.validateSignature(callbackData);
380
+
381
+ assert.strictEqual(result.isValid, false);
382
+ assert.strictEqual(result.error, 'Signature mismatch');
383
+ });
384
+
385
+ it('should return error result on exception', () => {
386
+ wayForPayService = createMockWayForPayService({
387
+ verifyCallbackSignature: mock.fn(() => {
388
+ throw new Error('Verification error');
389
+ }),
390
+ });
391
+ service = new PaymentService({ logger, repository, wayForPayService });
392
+
393
+ const callbackData = createMockCallbackData();
394
+ const result = service.validateSignature(callbackData);
395
+
396
+ assert.strictEqual(result.isValid, false);
397
+ assert.strictEqual(result.error, 'Error validating signature');
398
+ });
399
+ });
400
+ });