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,733 @@
1
+ import { prisma } from '../../database/prisma.js';
2
+ import type { PaginatedResult, PaginationParams } from '../../types/index.js';
3
+ import { createPaginatedResult, getSkip } from '../../utils/pagination.js';
4
+ import type { Payment, Subscription, Plan } from './types.js';
5
+ import {
6
+ PaymentProvider,
7
+ PaymentStatus,
8
+ PaymentMethod,
9
+ SubscriptionStatus,
10
+ PlanInterval,
11
+ } from '@prisma/client';
12
+
13
+ /**
14
+ * Payment Repository - Prisma Implementation
15
+ * Manages payment, subscription, and plan persistence
16
+ */
17
+ export class PaymentRepository {
18
+ // ==========================================
19
+ // PAYMENT METHODS
20
+ // ==========================================
21
+
22
+ /**
23
+ * Create a new payment
24
+ */
25
+ async createPayment(data: {
26
+ userId: string;
27
+ provider: Payment['provider'];
28
+ method: Payment['method'];
29
+ amount: number;
30
+ currency: string;
31
+ description?: string;
32
+ metadata?: Record<string, unknown>;
33
+ providerPaymentId?: string;
34
+ providerCustomerId?: string;
35
+ }): Promise<Payment> {
36
+ const payment = await prisma.payment.create({
37
+ data: {
38
+ userId: data.userId,
39
+ provider: this.mapProviderToEnum(data.provider),
40
+ method: this.mapMethodToEnum(data.method),
41
+ status: PaymentStatus.PENDING,
42
+ amount: data.amount,
43
+ currency: data.currency,
44
+ description: data.description,
45
+ metadata: data.metadata as object | undefined,
46
+ providerPaymentId: data.providerPaymentId,
47
+ providerCustomerId: data.providerCustomerId,
48
+ },
49
+ });
50
+
51
+ return this.mapPrismaPaymentToPayment(payment);
52
+ }
53
+
54
+ /**
55
+ * Find payment by ID
56
+ */
57
+ async findPaymentById(id: string): Promise<Payment | null> {
58
+ const payment = await prisma.payment.findUnique({
59
+ where: { id },
60
+ });
61
+
62
+ if (!payment) return null;
63
+ return this.mapPrismaPaymentToPayment(payment);
64
+ }
65
+
66
+ /**
67
+ * Find payment by provider payment ID
68
+ */
69
+ async findPaymentByProviderPaymentId(providerPaymentId: string): Promise<Payment | null> {
70
+ const payment = await prisma.payment.findUnique({
71
+ where: { providerPaymentId },
72
+ });
73
+
74
+ if (!payment) return null;
75
+ return this.mapPrismaPaymentToPayment(payment);
76
+ }
77
+
78
+ /**
79
+ * Find user payments with pagination
80
+ */
81
+ async findUserPayments(
82
+ userId: string,
83
+ params: PaginationParams
84
+ ): Promise<PaginatedResult<Payment>> {
85
+ const where = { userId };
86
+ const orderBy = { createdAt: 'desc' as const };
87
+
88
+ const [data, total] = await Promise.all([
89
+ prisma.payment.findMany({
90
+ where,
91
+ orderBy,
92
+ skip: getSkip(params),
93
+ take: params.limit,
94
+ }),
95
+ prisma.payment.count({ where }),
96
+ ]);
97
+
98
+ const mappedPayments = data.map((payment) => this.mapPrismaPaymentToPayment(payment));
99
+
100
+ return createPaginatedResult(mappedPayments, total, params);
101
+ }
102
+
103
+ /**
104
+ * Find payments with filters and pagination
105
+ */
106
+ async findPayments(
107
+ params: PaginationParams,
108
+ filters?: {
109
+ userId?: string;
110
+ provider?: Payment['provider'];
111
+ status?: Payment['status'];
112
+ startDate?: Date;
113
+ endDate?: Date;
114
+ }
115
+ ): Promise<PaginatedResult<Payment>> {
116
+ const where: Record<string, unknown> = {};
117
+
118
+ if (filters?.userId) where.userId = filters.userId;
119
+ if (filters?.provider) where.provider = this.mapProviderToEnum(filters.provider);
120
+ if (filters?.status) where.status = this.mapStatusToEnum(filters.status);
121
+ if (filters?.startDate || filters?.endDate) {
122
+ where.createdAt = {
123
+ ...(filters.startDate && { gte: filters.startDate }),
124
+ ...(filters.endDate && { lte: filters.endDate }),
125
+ };
126
+ }
127
+
128
+ const orderBy = { createdAt: 'desc' as const };
129
+
130
+ const [data, total] = await Promise.all([
131
+ prisma.payment.findMany({
132
+ where,
133
+ orderBy,
134
+ skip: getSkip(params),
135
+ take: params.limit,
136
+ }),
137
+ prisma.payment.count({ where }),
138
+ ]);
139
+
140
+ const mappedPayments = data.map((payment) => this.mapPrismaPaymentToPayment(payment));
141
+
142
+ return createPaginatedResult(mappedPayments, total, params);
143
+ }
144
+
145
+ /**
146
+ * Update payment status
147
+ */
148
+ async updatePaymentStatus(
149
+ id: string,
150
+ status: Payment['status'],
151
+ data?: {
152
+ paidAt?: Date;
153
+ failureReason?: string;
154
+ refundedAmount?: number;
155
+ }
156
+ ): Promise<Payment | null> {
157
+ try {
158
+ const payment = await prisma.payment.update({
159
+ where: { id },
160
+ data: {
161
+ status: this.mapStatusToEnum(status),
162
+ ...(data?.paidAt && { paidAt: data.paidAt }),
163
+ ...(data?.failureReason && { failureReason: data.failureReason }),
164
+ ...(data?.refundedAmount !== undefined && { refundedAmount: data.refundedAmount }),
165
+ },
166
+ });
167
+
168
+ return this.mapPrismaPaymentToPayment(payment);
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Delete payment by ID
176
+ */
177
+ async deletePayment(id: string): Promise<boolean> {
178
+ try {
179
+ await prisma.payment.delete({
180
+ where: { id },
181
+ });
182
+ return true;
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Count payments with filters
190
+ */
191
+ async countPayments(filters?: {
192
+ userId?: string;
193
+ provider?: Payment['provider'];
194
+ status?: Payment['status'];
195
+ }): Promise<number> {
196
+ const where: Record<string, unknown> = {};
197
+
198
+ if (filters?.userId) where.userId = filters.userId;
199
+ if (filters?.provider) where.provider = this.mapProviderToEnum(filters.provider);
200
+ if (filters?.status) where.status = this.mapStatusToEnum(filters.status);
201
+
202
+ return prisma.payment.count({ where });
203
+ }
204
+
205
+ // ==========================================
206
+ // SUBSCRIPTION METHODS
207
+ // ==========================================
208
+
209
+ /**
210
+ * Create a new subscription
211
+ */
212
+ async createSubscription(data: {
213
+ userId: string;
214
+ planId: string;
215
+ provider: Subscription['provider'];
216
+ currentPeriodStart: Date;
217
+ currentPeriodEnd: Date;
218
+ providerSubscriptionId?: string;
219
+ }): Promise<Subscription> {
220
+ const subscription = await prisma.subscription.create({
221
+ data: {
222
+ userId: data.userId,
223
+ planId: data.planId,
224
+ provider: this.mapProviderToEnum(data.provider),
225
+ status: SubscriptionStatus.ACTIVE,
226
+ currentPeriodStart: data.currentPeriodStart,
227
+ currentPeriodEnd: data.currentPeriodEnd,
228
+ providerSubscriptionId: data.providerSubscriptionId,
229
+ cancelAtPeriodEnd: false,
230
+ },
231
+ include: {
232
+ plan: true,
233
+ },
234
+ });
235
+
236
+ return this.mapPrismaSubscriptionToSubscription(subscription);
237
+ }
238
+
239
+ /**
240
+ * Find subscription by ID
241
+ */
242
+ async findSubscriptionById(id: string): Promise<Subscription | null> {
243
+ const subscription = await prisma.subscription.findUnique({
244
+ where: { id },
245
+ include: {
246
+ plan: true,
247
+ },
248
+ });
249
+
250
+ if (!subscription) return null;
251
+ return this.mapPrismaSubscriptionToSubscription(subscription);
252
+ }
253
+
254
+ /**
255
+ * Find user subscriptions
256
+ */
257
+ async findUserSubscriptions(userId: string): Promise<Subscription[]> {
258
+ const subscriptions = await prisma.subscription.findMany({
259
+ where: { userId },
260
+ include: {
261
+ plan: true,
262
+ },
263
+ orderBy: { createdAt: 'desc' },
264
+ });
265
+
266
+ return subscriptions.map((sub) => this.mapPrismaSubscriptionToSubscription(sub));
267
+ }
268
+
269
+ /**
270
+ * Update subscription status
271
+ */
272
+ async updateSubscriptionStatus(
273
+ id: string,
274
+ status: Subscription['status']
275
+ ): Promise<Subscription | null> {
276
+ try {
277
+ const subscription = await prisma.subscription.update({
278
+ where: { id },
279
+ data: {
280
+ status: this.mapSubscriptionStatusToEnum(status),
281
+ },
282
+ include: {
283
+ plan: true,
284
+ },
285
+ });
286
+
287
+ return this.mapPrismaSubscriptionToSubscription(subscription);
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Cancel subscription
295
+ */
296
+ async cancelSubscription(id: string): Promise<Subscription | null> {
297
+ try {
298
+ const subscription = await prisma.subscription.update({
299
+ where: { id },
300
+ data: {
301
+ cancelAtPeriodEnd: true,
302
+ },
303
+ include: {
304
+ plan: true,
305
+ },
306
+ });
307
+
308
+ return this.mapPrismaSubscriptionToSubscription(subscription);
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Delete subscription by ID
316
+ */
317
+ async deleteSubscription(id: string): Promise<boolean> {
318
+ try {
319
+ await prisma.subscription.delete({
320
+ where: { id },
321
+ });
322
+ return true;
323
+ } catch {
324
+ return false;
325
+ }
326
+ }
327
+
328
+ // ==========================================
329
+ // PLAN METHODS
330
+ // ==========================================
331
+
332
+ /**
333
+ * Create a new plan
334
+ */
335
+ async createPlan(data: {
336
+ name: string;
337
+ description?: string;
338
+ amount: number;
339
+ currency: string;
340
+ interval: Plan['interval'];
341
+ intervalCount: number;
342
+ trialDays?: number;
343
+ features?: string[];
344
+ metadata?: Record<string, unknown>;
345
+ active?: boolean;
346
+ }): Promise<Plan> {
347
+ const plan = await prisma.plan.create({
348
+ data: {
349
+ name: data.name,
350
+ description: data.description,
351
+ amount: data.amount,
352
+ currency: data.currency,
353
+ interval: this.mapIntervalToEnum(data.interval),
354
+ intervalCount: data.intervalCount,
355
+ trialDays: data.trialDays,
356
+ features: data.features as object | undefined,
357
+ metadata: data.metadata as object | undefined,
358
+ active: data.active ?? true,
359
+ },
360
+ });
361
+
362
+ return this.mapPrismaPlanToPlan(plan);
363
+ }
364
+
365
+ /**
366
+ * Find plan by ID
367
+ */
368
+ async findPlanById(id: string): Promise<Plan | null> {
369
+ const plan = await prisma.plan.findUnique({
370
+ where: { id },
371
+ });
372
+
373
+ if (!plan) return null;
374
+ return this.mapPrismaPlanToPlan(plan);
375
+ }
376
+
377
+ /**
378
+ * Find all active plans
379
+ */
380
+ async findActivePlans(): Promise<Plan[]> {
381
+ const plans = await prisma.plan.findMany({
382
+ where: { active: true },
383
+ orderBy: { amount: 'asc' },
384
+ });
385
+
386
+ return plans.map((plan) => this.mapPrismaPlanToPlan(plan));
387
+ }
388
+
389
+ /**
390
+ * Update plan
391
+ */
392
+ async updatePlan(
393
+ id: string,
394
+ data: {
395
+ name?: string;
396
+ description?: string;
397
+ amount?: number;
398
+ currency?: string;
399
+ active?: boolean;
400
+ features?: string[];
401
+ metadata?: Record<string, unknown>;
402
+ }
403
+ ): Promise<Plan | null> {
404
+ try {
405
+ const plan = await prisma.plan.update({
406
+ where: { id },
407
+ data: {
408
+ ...(data.name && { name: data.name }),
409
+ ...(data.description !== undefined && { description: data.description }),
410
+ ...(data.amount !== undefined && { amount: data.amount }),
411
+ ...(data.currency && { currency: data.currency }),
412
+ ...(data.active !== undefined && { active: data.active }),
413
+ ...(data.features && { features: data.features as object }),
414
+ ...(data.metadata && { metadata: data.metadata as object }),
415
+ },
416
+ });
417
+
418
+ return this.mapPrismaPlanToPlan(plan);
419
+ } catch {
420
+ return null;
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Delete plan by ID
426
+ */
427
+ async deletePlan(id: string): Promise<boolean> {
428
+ try {
429
+ await prisma.plan.delete({
430
+ where: { id },
431
+ });
432
+ return true;
433
+ } catch {
434
+ return false;
435
+ }
436
+ }
437
+
438
+ // ==========================================
439
+ // WEBHOOK METHODS
440
+ // ==========================================
441
+
442
+ /**
443
+ * Store webhook event
444
+ */
445
+ async storeWebhookEvent(data: {
446
+ provider: Payment['provider'];
447
+ type: string;
448
+ data: Record<string, unknown>;
449
+ }): Promise<{ id: string }> {
450
+ const webhook = await prisma.paymentWebhook.create({
451
+ data: {
452
+ provider: this.mapProviderToEnum(data.provider),
453
+ type: data.type,
454
+ data: data.data as object,
455
+ processed: false,
456
+ },
457
+ });
458
+
459
+ return { id: webhook.id };
460
+ }
461
+
462
+ /**
463
+ * Mark webhook as processed
464
+ */
465
+ async markWebhookProcessed(id: string, error?: string): Promise<void> {
466
+ await prisma.paymentWebhook.update({
467
+ where: { id },
468
+ data: {
469
+ processed: true,
470
+ ...(error && { error }),
471
+ },
472
+ });
473
+ }
474
+
475
+ // ==========================================
476
+ // TESTING UTILITIES
477
+ // ==========================================
478
+
479
+ /**
480
+ * Clear all payments (for testing only)
481
+ */
482
+ async clearPayments(): Promise<void> {
483
+ await prisma.payment.deleteMany();
484
+ }
485
+
486
+ /**
487
+ * Clear all subscriptions (for testing only)
488
+ */
489
+ async clearSubscriptions(): Promise<void> {
490
+ await prisma.subscription.deleteMany();
491
+ }
492
+
493
+ /**
494
+ * Clear all plans (for testing only)
495
+ */
496
+ async clearPlans(): Promise<void> {
497
+ await prisma.plan.deleteMany();
498
+ }
499
+
500
+ /**
501
+ * Clear all webhooks (for testing only)
502
+ */
503
+ async clearWebhooks(): Promise<void> {
504
+ await prisma.paymentWebhook.deleteMany();
505
+ }
506
+
507
+ // ==========================================
508
+ // PRIVATE MAPPING METHODS
509
+ // ==========================================
510
+
511
+ private mapPrismaPaymentToPayment(prismaPayment: {
512
+ id: string;
513
+ userId: string;
514
+ provider: PaymentProvider;
515
+ method: PaymentMethod;
516
+ status: PaymentStatus;
517
+ amount: number;
518
+ currency: string;
519
+ description: string | null;
520
+ metadata: unknown;
521
+ providerPaymentId: string | null;
522
+ providerCustomerId: string | null;
523
+ refundedAmount: number | null;
524
+ failureReason: string | null;
525
+ paidAt: Date | null;
526
+ createdAt: Date;
527
+ updatedAt: Date;
528
+ }): Payment {
529
+ return {
530
+ id: prismaPayment.id,
531
+ userId: prismaPayment.userId,
532
+ provider: this.mapEnumToProvider(prismaPayment.provider),
533
+ method: this.mapEnumToMethod(prismaPayment.method),
534
+ status: this.mapEnumToStatus(prismaPayment.status),
535
+ amount: prismaPayment.amount,
536
+ currency: prismaPayment.currency,
537
+ description: prismaPayment.description ?? undefined,
538
+ metadata: prismaPayment.metadata as Record<string, unknown> | undefined,
539
+ providerPaymentId: prismaPayment.providerPaymentId ?? undefined,
540
+ providerCustomerId: prismaPayment.providerCustomerId ?? undefined,
541
+ refundedAmount: prismaPayment.refundedAmount ?? undefined,
542
+ failureReason: prismaPayment.failureReason ?? undefined,
543
+ paidAt: prismaPayment.paidAt ?? undefined,
544
+ createdAt: prismaPayment.createdAt,
545
+ updatedAt: prismaPayment.updatedAt,
546
+ };
547
+ }
548
+
549
+ private mapPrismaSubscriptionToSubscription(prismaSubscription: {
550
+ id: string;
551
+ userId: string;
552
+ planId: string;
553
+ provider: PaymentProvider;
554
+ providerSubscriptionId: string | null;
555
+ status: SubscriptionStatus;
556
+ currentPeriodStart: Date;
557
+ currentPeriodEnd: Date;
558
+ cancelAtPeriodEnd: boolean;
559
+ createdAt: Date;
560
+ updatedAt: Date;
561
+ plan?: {
562
+ id: string;
563
+ name: string;
564
+ amount: number;
565
+ currency: string;
566
+ interval: PlanInterval;
567
+ intervalCount: number;
568
+ };
569
+ }): Subscription {
570
+ return {
571
+ id: prismaSubscription.id,
572
+ userId: prismaSubscription.userId,
573
+ planId: prismaSubscription.planId,
574
+ provider: this.mapEnumToProvider(prismaSubscription.provider),
575
+ providerSubscriptionId: prismaSubscription.providerSubscriptionId ?? undefined,
576
+ status: this.mapEnumToSubscriptionStatus(prismaSubscription.status),
577
+ currentPeriodStart: prismaSubscription.currentPeriodStart,
578
+ currentPeriodEnd: prismaSubscription.currentPeriodEnd,
579
+ cancelAtPeriodEnd: prismaSubscription.cancelAtPeriodEnd,
580
+ createdAt: prismaSubscription.createdAt,
581
+ updatedAt: prismaSubscription.updatedAt,
582
+ };
583
+ }
584
+
585
+ private mapPrismaPlanToPlan(prismaPlan: {
586
+ id: string;
587
+ name: string;
588
+ description: string | null;
589
+ amount: number;
590
+ currency: string;
591
+ interval: PlanInterval;
592
+ intervalCount: number;
593
+ trialDays: number | null;
594
+ features: unknown;
595
+ metadata: unknown;
596
+ active: boolean;
597
+ createdAt: Date;
598
+ updatedAt: Date;
599
+ }): Plan {
600
+ return {
601
+ id: prismaPlan.id,
602
+ name: prismaPlan.name,
603
+ description: prismaPlan.description ?? undefined,
604
+ amount: prismaPlan.amount,
605
+ currency: prismaPlan.currency,
606
+ interval: this.mapEnumToInterval(prismaPlan.interval),
607
+ intervalCount: prismaPlan.intervalCount,
608
+ trialDays: prismaPlan.trialDays ?? undefined,
609
+ features: prismaPlan.features as string[] | undefined,
610
+ metadata: prismaPlan.metadata as Record<string, unknown> | undefined,
611
+ active: prismaPlan.active,
612
+ };
613
+ }
614
+
615
+ // Provider mapping
616
+ private mapProviderToEnum(provider: string): PaymentProvider {
617
+ const providerMap: Record<string, PaymentProvider> = {
618
+ stripe: PaymentProvider.STRIPE,
619
+ paypal: PaymentProvider.PAYPAL,
620
+ mobile_money: PaymentProvider.MOBILE_MONEY,
621
+ manual: PaymentProvider.MANUAL,
622
+ };
623
+ return providerMap[provider] || PaymentProvider.MANUAL;
624
+ }
625
+
626
+ private mapEnumToProvider(provider: PaymentProvider): Payment['provider'] {
627
+ const providerMap: Record<PaymentProvider, Payment['provider']> = {
628
+ [PaymentProvider.STRIPE]: 'stripe',
629
+ [PaymentProvider.PAYPAL]: 'paypal',
630
+ [PaymentProvider.MOBILE_MONEY]: 'mobile_money',
631
+ [PaymentProvider.MANUAL]: 'manual',
632
+ };
633
+ return providerMap[provider];
634
+ }
635
+
636
+ // Status mapping
637
+ private mapStatusToEnum(status: string): PaymentStatus {
638
+ const statusMap: Record<string, PaymentStatus> = {
639
+ pending: PaymentStatus.PENDING,
640
+ processing: PaymentStatus.PROCESSING,
641
+ completed: PaymentStatus.COMPLETED,
642
+ failed: PaymentStatus.FAILED,
643
+ refunded: PaymentStatus.REFUNDED,
644
+ cancelled: PaymentStatus.CANCELLED,
645
+ };
646
+ return statusMap[status] || PaymentStatus.PENDING;
647
+ }
648
+
649
+ private mapEnumToStatus(status: PaymentStatus): Payment['status'] {
650
+ const statusMap: Record<PaymentStatus, Payment['status']> = {
651
+ [PaymentStatus.PENDING]: 'pending',
652
+ [PaymentStatus.PROCESSING]: 'processing',
653
+ [PaymentStatus.COMPLETED]: 'completed',
654
+ [PaymentStatus.FAILED]: 'failed',
655
+ [PaymentStatus.REFUNDED]: 'refunded',
656
+ [PaymentStatus.CANCELLED]: 'cancelled',
657
+ };
658
+ return statusMap[status];
659
+ }
660
+
661
+ // Method mapping
662
+ private mapMethodToEnum(method: string): PaymentMethod {
663
+ const methodMap: Record<string, PaymentMethod> = {
664
+ card: PaymentMethod.CARD,
665
+ bank_transfer: PaymentMethod.BANK_TRANSFER,
666
+ mobile_money: PaymentMethod.MOBILE_MONEY,
667
+ paypal: PaymentMethod.PAYPAL,
668
+ crypto: PaymentMethod.CRYPTO,
669
+ cash: PaymentMethod.CASH,
670
+ };
671
+ return methodMap[method] || PaymentMethod.CARD;
672
+ }
673
+
674
+ private mapEnumToMethod(method: PaymentMethod): Payment['method'] {
675
+ const methodMap: Record<PaymentMethod, Payment['method']> = {
676
+ [PaymentMethod.CARD]: 'card',
677
+ [PaymentMethod.BANK_TRANSFER]: 'bank_transfer',
678
+ [PaymentMethod.MOBILE_MONEY]: 'mobile_money',
679
+ [PaymentMethod.PAYPAL]: 'paypal',
680
+ [PaymentMethod.CRYPTO]: 'crypto',
681
+ [PaymentMethod.CASH]: 'cash',
682
+ };
683
+ return methodMap[method];
684
+ }
685
+
686
+ // Subscription status mapping
687
+ private mapSubscriptionStatusToEnum(status: string): SubscriptionStatus {
688
+ const statusMap: Record<string, SubscriptionStatus> = {
689
+ active: SubscriptionStatus.ACTIVE,
690
+ cancelled: SubscriptionStatus.CANCELLED,
691
+ past_due: SubscriptionStatus.PAST_DUE,
692
+ trialing: SubscriptionStatus.TRIALING,
693
+ paused: SubscriptionStatus.PAUSED,
694
+ };
695
+ return statusMap[status] || SubscriptionStatus.ACTIVE;
696
+ }
697
+
698
+ private mapEnumToSubscriptionStatus(status: SubscriptionStatus): Subscription['status'] {
699
+ const statusMap: Record<SubscriptionStatus, Subscription['status']> = {
700
+ [SubscriptionStatus.ACTIVE]: 'active',
701
+ [SubscriptionStatus.CANCELLED]: 'cancelled',
702
+ [SubscriptionStatus.PAST_DUE]: 'past_due',
703
+ [SubscriptionStatus.TRIALING]: 'trialing',
704
+ [SubscriptionStatus.PAUSED]: 'paused',
705
+ };
706
+ return statusMap[status];
707
+ }
708
+
709
+ // Interval mapping
710
+ private mapIntervalToEnum(interval: string): PlanInterval {
711
+ const intervalMap: Record<string, PlanInterval> = {
712
+ day: PlanInterval.DAY,
713
+ week: PlanInterval.WEEK,
714
+ month: PlanInterval.MONTH,
715
+ year: PlanInterval.YEAR,
716
+ };
717
+ return intervalMap[interval] || PlanInterval.MONTH;
718
+ }
719
+
720
+ private mapEnumToInterval(interval: PlanInterval): Plan['interval'] {
721
+ const intervalMap: Record<PlanInterval, Plan['interval']> = {
722
+ [PlanInterval.DAY]: 'day',
723
+ [PlanInterval.WEEK]: 'week',
724
+ [PlanInterval.MONTH]: 'month',
725
+ [PlanInterval.YEAR]: 'year',
726
+ };
727
+ return intervalMap[interval];
728
+ }
729
+ }
730
+
731
+ export function createPaymentRepository(): PaymentRepository {
732
+ return new PaymentRepository();
733
+ }