servcraft 0.1.0 → 0.1.3

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 (217) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,354 @@
1
+ import { logger } from '../../core/logger.js';
2
+ import { NotFoundError, BadRequestError } from '../../utils/errors.js';
3
+ import type {
4
+ Payment,
5
+ PaymentIntent,
6
+ CreatePaymentData,
7
+ PaymentProvider,
8
+ StripeConfig,
9
+ PayPalConfig,
10
+ MobileMoneyConfig,
11
+ Subscription,
12
+ Plan,
13
+ } from './types.js';
14
+ import { StripeProvider } from './providers/stripe.provider.js';
15
+ import { PayPalProvider } from './providers/paypal.provider.js';
16
+ import { MobileMoneyProvider } from './providers/mobile-money.provider.js';
17
+ import type { PaymentRepository } from './payment.repository.js';
18
+ import { createPaymentRepository } from './payment.repository.js';
19
+
20
+ export interface PaymentServiceConfig {
21
+ stripe?: StripeConfig;
22
+ paypal?: PayPalConfig;
23
+ mobileMoney?: MobileMoneyConfig;
24
+ }
25
+
26
+ export class PaymentService {
27
+ private stripeProvider?: StripeProvider;
28
+ private paypalProvider?: PayPalProvider;
29
+ private mobileMoneyProvider?: MobileMoneyProvider;
30
+ private repository: PaymentRepository;
31
+
32
+ constructor(config: PaymentServiceConfig = {}) {
33
+ this.repository = createPaymentRepository();
34
+
35
+ if (config.stripe) {
36
+ this.stripeProvider = new StripeProvider(config.stripe);
37
+ }
38
+ if (config.paypal) {
39
+ this.paypalProvider = new PayPalProvider(config.paypal);
40
+ }
41
+ if (config.mobileMoney) {
42
+ this.mobileMoneyProvider = new MobileMoneyProvider(config.mobileMoney);
43
+ }
44
+ }
45
+
46
+ // Payment methods
47
+ async createPayment(data: CreatePaymentData): Promise<PaymentIntent> {
48
+ let providerPaymentId: string | undefined;
49
+ let intent: PaymentIntent;
50
+
51
+ switch (data.provider) {
52
+ case 'stripe':
53
+ if (!this.stripeProvider) {
54
+ throw new BadRequestError('Stripe not configured');
55
+ }
56
+ intent = await this.stripeProvider.createPaymentIntent({
57
+ amount: data.amount,
58
+ currency: data.currency,
59
+ description: data.description,
60
+ metadata: data.metadata as Record<string, string>,
61
+ });
62
+ providerPaymentId = intent.id;
63
+ break;
64
+
65
+ case 'paypal':
66
+ if (!this.paypalProvider) {
67
+ throw new BadRequestError('PayPal not configured');
68
+ }
69
+ intent = await this.paypalProvider.createOrder({
70
+ amount: data.amount,
71
+ currency: data.currency,
72
+ description: data.description,
73
+ returnUrl: data.returnUrl || '',
74
+ cancelUrl: data.cancelUrl || '',
75
+ });
76
+ providerPaymentId = intent.id;
77
+ break;
78
+
79
+ case 'mobile_money': {
80
+ if (!this.mobileMoneyProvider) {
81
+ throw new BadRequestError('Mobile Money not configured');
82
+ }
83
+ const phoneNumber = data.metadata?.phoneNumber as string;
84
+ if (!phoneNumber) {
85
+ throw new BadRequestError('Phone number required for Mobile Money');
86
+ }
87
+ intent = await this.mobileMoneyProvider.initiatePayment({
88
+ amount: data.amount,
89
+ currency: data.currency,
90
+ phoneNumber,
91
+ description: data.description,
92
+ });
93
+ providerPaymentId = intent.id;
94
+ break;
95
+ }
96
+
97
+ default:
98
+ intent = {
99
+ id: '', // Will be set after payment creation
100
+ status: 'pending',
101
+ provider: 'manual',
102
+ };
103
+ }
104
+
105
+ // Create payment in database
106
+ const payment = await this.repository.createPayment({
107
+ userId: data.userId,
108
+ provider: data.provider,
109
+ method: data.method || 'card',
110
+ amount: data.amount,
111
+ currency: data.currency,
112
+ description: data.description,
113
+ metadata: data.metadata,
114
+ providerPaymentId,
115
+ });
116
+
117
+ logger.info({ paymentId: payment.id, provider: data.provider }, 'Payment created');
118
+
119
+ return {
120
+ ...intent,
121
+ id: payment.id,
122
+ };
123
+ }
124
+
125
+ async confirmPayment(paymentId: string): Promise<Payment> {
126
+ const payment = await this.repository.findPaymentById(paymentId);
127
+ if (!payment) {
128
+ throw new NotFoundError('Payment');
129
+ }
130
+
131
+ if (payment.provider === 'stripe' && this.stripeProvider && payment.providerPaymentId) {
132
+ await this.stripeProvider.confirmPayment(payment.providerPaymentId);
133
+ }
134
+
135
+ const updatedPayment = await this.repository.updatePaymentStatus(paymentId, 'completed', {
136
+ paidAt: new Date(),
137
+ });
138
+
139
+ if (!updatedPayment) {
140
+ throw new NotFoundError('Payment');
141
+ }
142
+
143
+ logger.info({ paymentId }, 'Payment confirmed');
144
+ return updatedPayment;
145
+ }
146
+
147
+ async refundPayment(paymentId: string, amount?: number): Promise<Payment> {
148
+ const payment = await this.repository.findPaymentById(paymentId);
149
+ if (!payment) {
150
+ throw new NotFoundError('Payment');
151
+ }
152
+
153
+ if (payment.status !== 'completed') {
154
+ throw new BadRequestError('Payment cannot be refunded');
155
+ }
156
+
157
+ const refundAmount = amount || payment.amount;
158
+
159
+ if (payment.provider === 'stripe' && this.stripeProvider && payment.providerPaymentId) {
160
+ await this.stripeProvider.refundPayment(payment.providerPaymentId, refundAmount);
161
+ } else if (payment.provider === 'paypal' && this.paypalProvider && payment.providerPaymentId) {
162
+ await this.paypalProvider.refundPayment(
163
+ payment.providerPaymentId,
164
+ refundAmount,
165
+ payment.currency
166
+ );
167
+ }
168
+
169
+ const updatedPayment = await this.repository.updatePaymentStatus(paymentId, 'refunded', {
170
+ refundedAmount: refundAmount,
171
+ });
172
+
173
+ if (!updatedPayment) {
174
+ throw new NotFoundError('Payment');
175
+ }
176
+
177
+ logger.info({ paymentId, refundAmount }, 'Payment refunded');
178
+ return updatedPayment;
179
+ }
180
+
181
+ async getPayment(paymentId: string): Promise<Payment | null> {
182
+ return this.repository.findPaymentById(paymentId);
183
+ }
184
+
185
+ async getUserPayments(userId: string): Promise<Payment[]> {
186
+ const result = await this.repository.findUserPayments(userId, { page: 1, limit: 100 });
187
+ return result.data;
188
+ }
189
+
190
+ // Subscription methods
191
+ async createSubscription(
192
+ userId: string,
193
+ planId: string,
194
+ provider: PaymentProvider = 'stripe'
195
+ ): Promise<Subscription> {
196
+ const plan = await this.repository.findPlanById(planId);
197
+ if (!plan) {
198
+ throw new NotFoundError('Plan');
199
+ }
200
+
201
+ const currentPeriodStart = new Date();
202
+ const currentPeriodEnd = this.calculatePeriodEnd(plan);
203
+
204
+ const subscription = await this.repository.createSubscription({
205
+ userId,
206
+ planId,
207
+ provider,
208
+ currentPeriodStart,
209
+ currentPeriodEnd,
210
+ });
211
+
212
+ logger.info({ subscriptionId: subscription.id, planId }, 'Subscription created');
213
+ return subscription;
214
+ }
215
+
216
+ async cancelSubscription(subscriptionId: string): Promise<Subscription> {
217
+ const subscription = await this.repository.cancelSubscription(subscriptionId);
218
+ if (!subscription) {
219
+ throw new NotFoundError('Subscription');
220
+ }
221
+
222
+ logger.info({ subscriptionId }, 'Subscription cancelled');
223
+ return subscription;
224
+ }
225
+
226
+ async getSubscription(subscriptionId: string): Promise<Subscription | null> {
227
+ return this.repository.findSubscriptionById(subscriptionId);
228
+ }
229
+
230
+ async getUserSubscriptions(userId: string): Promise<Subscription[]> {
231
+ return this.repository.findUserSubscriptions(userId);
232
+ }
233
+
234
+ // Plan methods
235
+ async createPlan(planData: Omit<Plan, 'id'>): Promise<Plan> {
236
+ const plan = await this.repository.createPlan({
237
+ name: planData.name,
238
+ description: planData.description,
239
+ amount: planData.amount,
240
+ currency: planData.currency,
241
+ interval: planData.interval,
242
+ intervalCount: planData.intervalCount,
243
+ trialDays: planData.trialDays,
244
+ features: planData.features,
245
+ metadata: planData.metadata,
246
+ active: planData.active,
247
+ });
248
+
249
+ logger.info({ planId: plan.id }, 'Plan created');
250
+ return plan;
251
+ }
252
+
253
+ async getPlans(): Promise<Plan[]> {
254
+ return this.repository.findActivePlans();
255
+ }
256
+
257
+ async getPlan(planId: string): Promise<Plan | null> {
258
+ return this.repository.findPlanById(planId);
259
+ }
260
+
261
+ // Webhook handling
262
+ async handleWebhook(
263
+ provider: PaymentProvider,
264
+ payload: string,
265
+ signature: string
266
+ ): Promise<void> {
267
+ logger.info({ provider }, 'Processing payment webhook');
268
+
269
+ switch (provider) {
270
+ case 'stripe':
271
+ if (this.stripeProvider) {
272
+ const event = this.stripeProvider.verifyWebhookSignature(payload, signature);
273
+ if (event) {
274
+ await this.processStripeEvent(event);
275
+ }
276
+ }
277
+ break;
278
+ case 'paypal':
279
+ // Handle PayPal webhook
280
+ break;
281
+ case 'mobile_money':
282
+ // Handle Mobile Money callback
283
+ break;
284
+ }
285
+ }
286
+
287
+ private async processStripeEvent(event: Record<string, unknown>): Promise<void> {
288
+ const type = event.type as string;
289
+ const data = event.data as { object: Record<string, unknown> };
290
+ const providerPaymentId = data.object.id as string;
291
+
292
+ // Store webhook event
293
+ await this.repository.storeWebhookEvent({
294
+ provider: 'stripe',
295
+ type,
296
+ data: data.object,
297
+ });
298
+
299
+ // Find payment by provider payment ID
300
+ const payment = await this.repository.findPaymentByProviderPaymentId(providerPaymentId);
301
+ if (!payment) {
302
+ logger.warn({ providerPaymentId, type }, 'Payment not found for webhook event');
303
+ return;
304
+ }
305
+
306
+ switch (type) {
307
+ case 'payment_intent.succeeded':
308
+ await this.repository.updatePaymentStatus(payment.id, 'completed', {
309
+ paidAt: new Date(),
310
+ });
311
+ logger.info({ paymentId: payment.id }, 'Payment completed via webhook');
312
+ break;
313
+
314
+ case 'payment_intent.payment_failed': {
315
+ const failureReason = (data.object.last_payment_error as { message?: string })?.message;
316
+ await this.repository.updatePaymentStatus(payment.id, 'failed', {
317
+ failureReason,
318
+ });
319
+ logger.info({ paymentId: payment.id, failureReason }, 'Payment failed via webhook');
320
+ break;
321
+ }
322
+ }
323
+ }
324
+
325
+ private calculatePeriodEnd(plan: Plan): Date {
326
+ const now = new Date();
327
+ switch (plan.interval) {
328
+ case 'day':
329
+ return new Date(now.setDate(now.getDate() + plan.intervalCount));
330
+ case 'week':
331
+ return new Date(now.setDate(now.getDate() + 7 * plan.intervalCount));
332
+ case 'month':
333
+ return new Date(now.setMonth(now.getMonth() + plan.intervalCount));
334
+ case 'year':
335
+ return new Date(now.setFullYear(now.getFullYear() + plan.intervalCount));
336
+ default:
337
+ return new Date(now.setMonth(now.getMonth() + 1));
338
+ }
339
+ }
340
+ }
341
+
342
+ let paymentService: PaymentService | null = null;
343
+
344
+ export function getPaymentService(): PaymentService {
345
+ if (!paymentService) {
346
+ paymentService = new PaymentService();
347
+ }
348
+ return paymentService;
349
+ }
350
+
351
+ export function createPaymentService(config: PaymentServiceConfig): PaymentService {
352
+ paymentService = new PaymentService(config);
353
+ return paymentService;
354
+ }
@@ -0,0 +1,274 @@
1
+ import { logger } from '../../../core/logger.js';
2
+ import type {
3
+ MobileMoneyConfig,
4
+ MobileMoneyPaymentData,
5
+ PaymentIntent,
6
+ Payment,
7
+ } from '../types.js';
8
+
9
+ /**
10
+ * Mobile Money Payment Provider
11
+ * Supports MTN MoMo, Orange Money, Wave, M-Pesa
12
+ */
13
+ export class MobileMoneyProvider {
14
+ private config: MobileMoneyConfig;
15
+
16
+ constructor(config: MobileMoneyConfig) {
17
+ this.config = config;
18
+ logger.info({ provider: config.provider }, 'Mobile Money provider initialized');
19
+ }
20
+
21
+ private get baseUrl(): string {
22
+ const urls: Record<string, Record<string, string>> = {
23
+ mtn: {
24
+ sandbox: 'https://sandbox.momodeveloper.mtn.com',
25
+ production: 'https://momodeveloper.mtn.com',
26
+ },
27
+ orange: {
28
+ sandbox: 'https://api.sandbox.orange.com',
29
+ production: 'https://api.orange.com',
30
+ },
31
+ wave: {
32
+ sandbox: 'https://api.sandbox.wave.com',
33
+ production: 'https://api.wave.com',
34
+ },
35
+ mpesa: {
36
+ sandbox: 'https://sandbox.safaricom.co.ke',
37
+ production: 'https://api.safaricom.co.ke',
38
+ },
39
+ };
40
+
41
+ return urls[this.config.provider]?.[this.config.environment] || '';
42
+ }
43
+
44
+ async initiatePayment(data: MobileMoneyPaymentData): Promise<PaymentIntent> {
45
+ switch (this.config.provider) {
46
+ case 'mtn':
47
+ return this.initiateMTNPayment(data);
48
+ case 'orange':
49
+ return this.initiateOrangePayment(data);
50
+ case 'wave':
51
+ return this.initiateWavePayment(data);
52
+ case 'mpesa':
53
+ return this.initiateMpesaPayment(data);
54
+ default:
55
+ throw new Error(`Unsupported provider: ${this.config.provider}`);
56
+ }
57
+ }
58
+
59
+ private async initiateMTNPayment(data: MobileMoneyPaymentData): Promise<PaymentIntent> {
60
+ // MTN MoMo Collection API
61
+ const referenceId = crypto.randomUUID();
62
+
63
+ const response = await fetch(`${this.baseUrl}/collection/v1_0/requesttopay`, {
64
+ method: 'POST',
65
+ headers: {
66
+ Authorization: `Bearer ${await this.getAccessToken()}`,
67
+ 'X-Reference-Id': referenceId,
68
+ 'X-Target-Environment': this.config.environment,
69
+ 'Ocp-Apim-Subscription-Key': this.config.apiKey,
70
+ 'Content-Type': 'application/json',
71
+ },
72
+ body: JSON.stringify({
73
+ amount: data.amount.toString(),
74
+ currency: data.currency,
75
+ externalId: data.reference || referenceId,
76
+ payer: {
77
+ partyIdType: 'MSISDN',
78
+ partyId: data.phoneNumber.replace('+', ''),
79
+ },
80
+ payerMessage: data.description || 'Payment',
81
+ payeeNote: data.description || 'Payment',
82
+ }),
83
+ });
84
+
85
+ if (!response.ok) {
86
+ const error = await response.text();
87
+ logger.error({ error }, 'MTN MoMo payment initiation failed');
88
+ throw new Error('Failed to initiate MTN payment');
89
+ }
90
+
91
+ return {
92
+ id: referenceId,
93
+ status: 'pending',
94
+ provider: 'mobile_money',
95
+ };
96
+ }
97
+
98
+ private async initiateOrangePayment(data: MobileMoneyPaymentData): Promise<PaymentIntent> {
99
+ // Orange Money API
100
+ const response = await fetch(`${this.baseUrl}/orange-money-webpay/dev/v1/webpayment`, {
101
+ method: 'POST',
102
+ headers: {
103
+ Authorization: `Bearer ${await this.getAccessToken()}`,
104
+ 'Content-Type': 'application/json',
105
+ },
106
+ body: JSON.stringify({
107
+ merchant_key: this.config.apiKey,
108
+ currency: data.currency,
109
+ order_id: data.reference || crypto.randomUUID(),
110
+ amount: data.amount,
111
+ return_url: this.config.callbackUrl,
112
+ cancel_url: this.config.callbackUrl,
113
+ notif_url: this.config.callbackUrl,
114
+ lang: 'fr',
115
+ }),
116
+ });
117
+
118
+ if (!response.ok) {
119
+ throw new Error('Failed to initiate Orange Money payment');
120
+ }
121
+
122
+ const result = (await response.json()) as { payment_url: string; pay_token: string };
123
+
124
+ return {
125
+ id: result.pay_token,
126
+ paymentUrl: result.payment_url,
127
+ status: 'pending',
128
+ provider: 'mobile_money',
129
+ };
130
+ }
131
+
132
+ private async initiateWavePayment(data: MobileMoneyPaymentData): Promise<PaymentIntent> {
133
+ // Wave API
134
+ const response = await fetch(`${this.baseUrl}/v1/checkout/sessions`, {
135
+ method: 'POST',
136
+ headers: {
137
+ Authorization: `Bearer ${this.config.apiSecret}`,
138
+ 'Content-Type': 'application/json',
139
+ },
140
+ body: JSON.stringify({
141
+ amount: data.amount.toString(),
142
+ currency: data.currency,
143
+ error_url: this.config.callbackUrl,
144
+ success_url: this.config.callbackUrl,
145
+ client_reference: data.reference,
146
+ }),
147
+ });
148
+
149
+ if (!response.ok) {
150
+ throw new Error('Failed to initiate Wave payment');
151
+ }
152
+
153
+ const result = (await response.json()) as { id: string; wave_launch_url: string };
154
+
155
+ return {
156
+ id: result.id,
157
+ paymentUrl: result.wave_launch_url,
158
+ status: 'pending',
159
+ provider: 'mobile_money',
160
+ };
161
+ }
162
+
163
+ private async initiateMpesaPayment(data: MobileMoneyPaymentData): Promise<PaymentIntent> {
164
+ // M-Pesa STK Push
165
+ const timestamp = new Date()
166
+ .toISOString()
167
+ .replace(/[-:T.Z]/g, '')
168
+ .slice(0, 14);
169
+ const password = Buffer.from(
170
+ `${this.config.apiKey}${this.config.apiSecret}${timestamp}`
171
+ ).toString('base64');
172
+
173
+ const response = await fetch(`${this.baseUrl}/mpesa/stkpush/v1/processrequest`, {
174
+ method: 'POST',
175
+ headers: {
176
+ Authorization: `Bearer ${await this.getAccessToken()}`,
177
+ 'Content-Type': 'application/json',
178
+ },
179
+ body: JSON.stringify({
180
+ BusinessShortCode: this.config.apiKey,
181
+ Password: password,
182
+ Timestamp: timestamp,
183
+ TransactionType: 'CustomerPayBillOnline',
184
+ Amount: Math.round(data.amount),
185
+ PartyA: data.phoneNumber.replace('+', ''),
186
+ PartyB: this.config.apiKey,
187
+ PhoneNumber: data.phoneNumber.replace('+', ''),
188
+ CallBackURL: this.config.callbackUrl,
189
+ AccountReference: data.reference || 'Payment',
190
+ TransactionDesc: data.description || 'Payment',
191
+ }),
192
+ });
193
+
194
+ if (!response.ok) {
195
+ throw new Error('Failed to initiate M-Pesa payment');
196
+ }
197
+
198
+ const result = (await response.json()) as { CheckoutRequestID: string };
199
+
200
+ return {
201
+ id: result.CheckoutRequestID,
202
+ status: 'pending',
203
+ provider: 'mobile_money',
204
+ };
205
+ }
206
+
207
+ async checkPaymentStatus(paymentId: string): Promise<Payment['status']> {
208
+ switch (this.config.provider) {
209
+ case 'mtn':
210
+ return this.checkMTNStatus(paymentId);
211
+ case 'mpesa':
212
+ return this.checkMpesaStatus(paymentId);
213
+ default:
214
+ return 'pending';
215
+ }
216
+ }
217
+
218
+ private async checkMTNStatus(referenceId: string): Promise<Payment['status']> {
219
+ const response = await fetch(`${this.baseUrl}/collection/v1_0/requesttopay/${referenceId}`, {
220
+ headers: {
221
+ Authorization: `Bearer ${await this.getAccessToken()}`,
222
+ 'X-Target-Environment': this.config.environment,
223
+ 'Ocp-Apim-Subscription-Key': this.config.apiKey,
224
+ },
225
+ });
226
+
227
+ if (!response.ok) {
228
+ return 'pending';
229
+ }
230
+
231
+ const result = (await response.json()) as { status: string };
232
+ const statusMap: Record<string, Payment['status']> = {
233
+ PENDING: 'pending',
234
+ SUCCESSFUL: 'completed',
235
+ FAILED: 'failed',
236
+ };
237
+
238
+ return statusMap[result.status] || 'pending';
239
+ }
240
+
241
+ private async checkMpesaStatus(_checkoutRequestId: string): Promise<Payment['status']> {
242
+ // M-Pesa query implementation
243
+ return 'pending';
244
+ }
245
+
246
+ private async getAccessToken(): Promise<string> {
247
+ // Token fetching logic varies by provider
248
+ // This is a simplified version
249
+ if (this.config.provider === 'mtn') {
250
+ const auth = Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64');
251
+
252
+ const response = await fetch(`${this.baseUrl}/collection/token/`, {
253
+ method: 'POST',
254
+ headers: {
255
+ Authorization: `Basic ${auth}`,
256
+ 'Ocp-Apim-Subscription-Key': this.config.apiKey,
257
+ },
258
+ });
259
+
260
+ if (!response.ok) {
261
+ throw new Error('Failed to get MTN access token');
262
+ }
263
+
264
+ const data = (await response.json()) as { access_token: string };
265
+ return data.access_token;
266
+ }
267
+
268
+ return this.config.apiSecret;
269
+ }
270
+ }
271
+
272
+ export function createMobileMoneyProvider(config: MobileMoneyConfig): MobileMoneyProvider {
273
+ return new MobileMoneyProvider(config);
274
+ }