servcraft 0.1.0 → 0.1.1

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 (216) hide show
  1. package/.claude/settings.local.json +29 -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/README.md +1070 -1
  9. package/dist/cli/index.cjs +2026 -2168
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +2026 -2168
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.cjs +595 -616
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +114 -52
  16. package/dist/index.d.ts +114 -52
  17. package/dist/index.js +595 -616
  18. package/dist/index.js.map +1 -1
  19. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  20. package/docs/DATABASE_MULTI_ORM.md +399 -0
  21. package/docs/PHASE1_BREAKDOWN.md +346 -0
  22. package/docs/PROGRESS.md +550 -0
  23. package/docs/modules/ANALYTICS.md +226 -0
  24. package/docs/modules/API-VERSIONING.md +252 -0
  25. package/docs/modules/AUDIT.md +192 -0
  26. package/docs/modules/AUTH.md +431 -0
  27. package/docs/modules/CACHE.md +346 -0
  28. package/docs/modules/EMAIL.md +254 -0
  29. package/docs/modules/FEATURE-FLAG.md +291 -0
  30. package/docs/modules/I18N.md +294 -0
  31. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  32. package/docs/modules/MFA.md +266 -0
  33. package/docs/modules/NOTIFICATION.md +311 -0
  34. package/docs/modules/OAUTH.md +237 -0
  35. package/docs/modules/PAYMENT.md +804 -0
  36. package/docs/modules/QUEUE.md +540 -0
  37. package/docs/modules/RATE-LIMIT.md +339 -0
  38. package/docs/modules/SEARCH.md +288 -0
  39. package/docs/modules/SECURITY.md +327 -0
  40. package/docs/modules/SESSION.md +382 -0
  41. package/docs/modules/SWAGGER.md +305 -0
  42. package/docs/modules/UPLOAD.md +296 -0
  43. package/docs/modules/USER.md +505 -0
  44. package/docs/modules/VALIDATION.md +294 -0
  45. package/docs/modules/WEBHOOK.md +270 -0
  46. package/docs/modules/WEBSOCKET.md +691 -0
  47. package/package.json +53 -38
  48. package/prisma/schema.prisma +395 -1
  49. package/src/cli/commands/add-module.ts +520 -87
  50. package/src/cli/commands/db.ts +3 -4
  51. package/src/cli/commands/docs.ts +256 -6
  52. package/src/cli/commands/generate.ts +12 -19
  53. package/src/cli/commands/init.ts +384 -214
  54. package/src/cli/index.ts +0 -4
  55. package/src/cli/templates/repository.ts +6 -1
  56. package/src/cli/templates/routes.ts +6 -21
  57. package/src/cli/utils/docs-generator.ts +6 -7
  58. package/src/cli/utils/env-manager.ts +717 -0
  59. package/src/cli/utils/field-parser.ts +16 -7
  60. package/src/cli/utils/interactive-prompt.ts +223 -0
  61. package/src/cli/utils/template-manager.ts +346 -0
  62. package/src/config/database.config.ts +183 -0
  63. package/src/config/env.ts +0 -10
  64. package/src/config/index.ts +0 -14
  65. package/src/core/server.ts +1 -1
  66. package/src/database/adapters/mongoose.adapter.ts +132 -0
  67. package/src/database/adapters/prisma.adapter.ts +118 -0
  68. package/src/database/connection.ts +190 -0
  69. package/src/database/interfaces/database.interface.ts +85 -0
  70. package/src/database/interfaces/index.ts +7 -0
  71. package/src/database/interfaces/repository.interface.ts +129 -0
  72. package/src/database/models/mongoose/index.ts +7 -0
  73. package/src/database/models/mongoose/payment.schema.ts +347 -0
  74. package/src/database/models/mongoose/user.schema.ts +154 -0
  75. package/src/database/prisma.ts +1 -4
  76. package/src/database/redis.ts +101 -0
  77. package/src/database/repositories/mongoose/index.ts +7 -0
  78. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  79. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  80. package/src/database/seed.ts +6 -1
  81. package/src/index.ts +9 -20
  82. package/src/middleware/security.ts +2 -6
  83. package/src/modules/analytics/analytics.routes.ts +80 -0
  84. package/src/modules/analytics/analytics.service.ts +364 -0
  85. package/src/modules/analytics/index.ts +18 -0
  86. package/src/modules/analytics/types.ts +180 -0
  87. package/src/modules/api-versioning/index.ts +15 -0
  88. package/src/modules/api-versioning/types.ts +86 -0
  89. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  90. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  91. package/src/modules/api-versioning/versioning.service.ts +189 -0
  92. package/src/modules/audit/audit.repository.ts +206 -0
  93. package/src/modules/audit/audit.service.ts +27 -59
  94. package/src/modules/auth/auth.controller.ts +2 -2
  95. package/src/modules/auth/auth.middleware.ts +3 -9
  96. package/src/modules/auth/auth.routes.ts +10 -107
  97. package/src/modules/auth/auth.service.ts +126 -23
  98. package/src/modules/auth/index.ts +3 -4
  99. package/src/modules/cache/cache.service.ts +367 -0
  100. package/src/modules/cache/index.ts +10 -0
  101. package/src/modules/cache/types.ts +44 -0
  102. package/src/modules/email/email.service.ts +3 -10
  103. package/src/modules/email/templates.ts +2 -8
  104. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  105. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  106. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  107. package/src/modules/feature-flag/index.ts +20 -0
  108. package/src/modules/feature-flag/types.ts +192 -0
  109. package/src/modules/i18n/i18n.middleware.ts +186 -0
  110. package/src/modules/i18n/i18n.routes.ts +191 -0
  111. package/src/modules/i18n/i18n.service.ts +456 -0
  112. package/src/modules/i18n/index.ts +18 -0
  113. package/src/modules/i18n/types.ts +118 -0
  114. package/src/modules/media-processing/index.ts +17 -0
  115. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  116. package/src/modules/media-processing/media-processing.service.ts +245 -0
  117. package/src/modules/media-processing/types.ts +156 -0
  118. package/src/modules/mfa/index.ts +20 -0
  119. package/src/modules/mfa/mfa.repository.ts +206 -0
  120. package/src/modules/mfa/mfa.routes.ts +595 -0
  121. package/src/modules/mfa/mfa.service.ts +572 -0
  122. package/src/modules/mfa/totp.ts +150 -0
  123. package/src/modules/mfa/types.ts +57 -0
  124. package/src/modules/notification/index.ts +20 -0
  125. package/src/modules/notification/notification.repository.ts +356 -0
  126. package/src/modules/notification/notification.service.ts +483 -0
  127. package/src/modules/notification/types.ts +119 -0
  128. package/src/modules/oauth/index.ts +20 -0
  129. package/src/modules/oauth/oauth.repository.ts +219 -0
  130. package/src/modules/oauth/oauth.routes.ts +446 -0
  131. package/src/modules/oauth/oauth.service.ts +293 -0
  132. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  133. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  134. package/src/modules/oauth/providers/github.provider.ts +248 -0
  135. package/src/modules/oauth/providers/google.provider.ts +189 -0
  136. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  137. package/src/modules/oauth/types.ts +94 -0
  138. package/src/modules/payment/index.ts +19 -0
  139. package/src/modules/payment/payment.repository.ts +733 -0
  140. package/src/modules/payment/payment.routes.ts +390 -0
  141. package/src/modules/payment/payment.service.ts +354 -0
  142. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  143. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  144. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  145. package/src/modules/payment/types.ts +140 -0
  146. package/src/modules/queue/cron.ts +438 -0
  147. package/src/modules/queue/index.ts +87 -0
  148. package/src/modules/queue/queue.routes.ts +600 -0
  149. package/src/modules/queue/queue.service.ts +842 -0
  150. package/src/modules/queue/types.ts +222 -0
  151. package/src/modules/queue/workers.ts +366 -0
  152. package/src/modules/rate-limit/index.ts +59 -0
  153. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  154. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  155. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  156. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  157. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  158. package/src/modules/rate-limit/types.ts +153 -0
  159. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  160. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  161. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  162. package/src/modules/search/index.ts +21 -0
  163. package/src/modules/search/search.service.ts +234 -0
  164. package/src/modules/search/types.ts +214 -0
  165. package/src/modules/security/index.ts +40 -0
  166. package/src/modules/security/sanitize.ts +223 -0
  167. package/src/modules/security/security-audit.service.ts +388 -0
  168. package/src/modules/security/security.middleware.ts +398 -0
  169. package/src/modules/session/index.ts +3 -0
  170. package/src/modules/session/session.repository.ts +159 -0
  171. package/src/modules/session/session.service.ts +340 -0
  172. package/src/modules/session/types.ts +38 -0
  173. package/src/modules/swagger/index.ts +7 -1
  174. package/src/modules/swagger/schema-builder.ts +16 -4
  175. package/src/modules/swagger/swagger.service.ts +9 -10
  176. package/src/modules/swagger/types.ts +0 -2
  177. package/src/modules/upload/index.ts +14 -0
  178. package/src/modules/upload/types.ts +83 -0
  179. package/src/modules/upload/upload.repository.ts +199 -0
  180. package/src/modules/upload/upload.routes.ts +311 -0
  181. package/src/modules/upload/upload.service.ts +448 -0
  182. package/src/modules/user/index.ts +3 -3
  183. package/src/modules/user/user.controller.ts +15 -9
  184. package/src/modules/user/user.repository.ts +237 -113
  185. package/src/modules/user/user.routes.ts +39 -164
  186. package/src/modules/user/user.service.ts +4 -3
  187. package/src/modules/validation/validator.ts +12 -17
  188. package/src/modules/webhook/index.ts +91 -0
  189. package/src/modules/webhook/retry.ts +196 -0
  190. package/src/modules/webhook/signature.ts +135 -0
  191. package/src/modules/webhook/types.ts +181 -0
  192. package/src/modules/webhook/webhook.repository.ts +358 -0
  193. package/src/modules/webhook/webhook.routes.ts +442 -0
  194. package/src/modules/webhook/webhook.service.ts +457 -0
  195. package/src/modules/websocket/features.ts +504 -0
  196. package/src/modules/websocket/index.ts +106 -0
  197. package/src/modules/websocket/middlewares.ts +298 -0
  198. package/src/modules/websocket/types.ts +181 -0
  199. package/src/modules/websocket/websocket.service.ts +692 -0
  200. package/src/utils/errors.ts +7 -0
  201. package/src/utils/pagination.ts +4 -1
  202. package/tests/helpers/db-check.ts +79 -0
  203. package/tests/integration/auth-redis.test.ts +94 -0
  204. package/tests/integration/cache-redis.test.ts +387 -0
  205. package/tests/integration/mongoose-repositories.test.ts +410 -0
  206. package/tests/integration/payment-prisma.test.ts +637 -0
  207. package/tests/integration/queue-bullmq.test.ts +417 -0
  208. package/tests/integration/user-prisma.test.ts +441 -0
  209. package/tests/integration/websocket-socketio.test.ts +552 -0
  210. package/tests/setup.ts +11 -9
  211. package/vitest.config.ts +3 -8
  212. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  213. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  216. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Mongoose User Schema
3
+ * MongoDB schema for User entity
4
+ */
5
+
6
+ import { Schema, model, type Document } from 'mongoose';
7
+ import bcrypt from 'bcryptjs';
8
+
9
+ /**
10
+ * User interface for Mongoose
11
+ */
12
+ export interface IUserDocument extends Document {
13
+ email: string;
14
+ password: string;
15
+ name: string;
16
+ role: 'user' | 'moderator' | 'admin' | 'super_admin';
17
+ status: 'active' | 'inactive' | 'suspended' | 'pending';
18
+ emailVerified: boolean;
19
+ emailVerifiedAt?: Date;
20
+ lastLoginAt?: Date;
21
+ metadata?: Record<string, unknown>;
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+
25
+ // Methods
26
+ comparePassword(candidatePassword: string): Promise<boolean>;
27
+ }
28
+
29
+ /**
30
+ * User Schema
31
+ */
32
+ const userSchema = new Schema<IUserDocument>(
33
+ {
34
+ email: {
35
+ type: String,
36
+ required: [true, 'Email is required'],
37
+ unique: true,
38
+ lowercase: true,
39
+ trim: true,
40
+ match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email'],
41
+ index: true,
42
+ },
43
+ password: {
44
+ type: String,
45
+ required: [true, 'Password is required'],
46
+ minlength: [6, 'Password must be at least 6 characters'],
47
+ select: false, // Don't include password by default
48
+ },
49
+ name: {
50
+ type: String,
51
+ required: [true, 'Name is required'],
52
+ trim: true,
53
+ maxlength: [100, 'Name cannot exceed 100 characters'],
54
+ },
55
+ role: {
56
+ type: String,
57
+ enum: {
58
+ values: ['user', 'moderator', 'admin', 'super_admin'],
59
+ message: '{VALUE} is not a valid role',
60
+ },
61
+ default: 'user',
62
+ index: true,
63
+ },
64
+ status: {
65
+ type: String,
66
+ enum: {
67
+ values: ['active', 'inactive', 'suspended', 'pending'],
68
+ message: '{VALUE} is not a valid status',
69
+ },
70
+ default: 'active',
71
+ index: true,
72
+ },
73
+ emailVerified: {
74
+ type: Boolean,
75
+ default: false,
76
+ },
77
+ emailVerifiedAt: {
78
+ type: Date,
79
+ default: null,
80
+ },
81
+ lastLoginAt: {
82
+ type: Date,
83
+ default: null,
84
+ },
85
+ metadata: {
86
+ type: Schema.Types.Mixed,
87
+ default: null,
88
+ },
89
+ },
90
+ {
91
+ timestamps: true, // Automatically add createdAt and updatedAt
92
+ collection: 'users',
93
+ toJSON: {
94
+ virtuals: true,
95
+ transform: (_doc, ret): Record<string, unknown> => {
96
+ const id = ret._id.toString();
97
+ const { password, _id: mongoId, __v, ...rest } = ret;
98
+ void password; // Intentionally excluded from output
99
+ void mongoId;
100
+ void __v;
101
+ return { id, ...rest };
102
+ },
103
+ },
104
+ toObject: {
105
+ virtuals: true,
106
+ transform: (_doc, ret): Record<string, unknown> => {
107
+ const { _id, ...rest } = ret;
108
+ return { id: _id.toString(), ...rest };
109
+ },
110
+ },
111
+ }
112
+ );
113
+
114
+ /**
115
+ * Pre-save middleware to hash password
116
+ */
117
+ userSchema.pre('save', async function (next) {
118
+ // Only hash the password if it has been modified (or is new)
119
+ if (!this.isModified('password')) {
120
+ return next();
121
+ }
122
+
123
+ try {
124
+ const salt = await bcrypt.genSalt(10);
125
+ this.password = await bcrypt.hash(this.password, salt);
126
+ next();
127
+ } catch (error) {
128
+ next(error as Error);
129
+ }
130
+ });
131
+
132
+ /**
133
+ * Method to compare password
134
+ */
135
+ userSchema.methods.comparePassword = async function (candidatePassword: string): Promise<boolean> {
136
+ try {
137
+ return await bcrypt.compare(candidatePassword, this.password);
138
+ } catch {
139
+ return false;
140
+ }
141
+ };
142
+
143
+ /**
144
+ * Indexes for better query performance
145
+ * Note: email index is already defined via unique: true in schema
146
+ * role and status indexes are defined via index: true in schema
147
+ */
148
+ userSchema.index({ role: 1, status: 1 }); // Compound index for filtering
149
+ userSchema.index({ createdAt: -1 }); // For sorting by creation date
150
+
151
+ /**
152
+ * User Model
153
+ */
154
+ export const UserModel = model<IUserDocument>('User', userSchema);
@@ -3,15 +3,12 @@ import { logger } from '../core/logger.js';
3
3
  import { isProduction } from '../config/index.js';
4
4
 
5
5
  declare global {
6
- // eslint-disable-next-line no-var
7
6
  var __prisma: PrismaClient | undefined;
8
7
  }
9
8
 
10
9
  const prismaClientSingleton = (): PrismaClient => {
11
10
  return new PrismaClient({
12
- log: isProduction()
13
- ? ['error']
14
- : ['query', 'info', 'warn', 'error'],
11
+ log: isProduction() ? ['error'] : ['query', 'info', 'warn', 'error'],
15
12
  errorFormat: isProduction() ? 'minimal' : 'pretty',
16
13
  });
17
14
  };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Redis Client Module
3
+ * Shared Redis connection for all services
4
+ */
5
+ import { Redis } from 'ioredis';
6
+ import { logger } from '../core/logger.js';
7
+
8
+ export interface RedisConfig {
9
+ host: string;
10
+ port: number;
11
+ password?: string;
12
+ db?: number;
13
+ tls?: boolean;
14
+ connectTimeout?: number;
15
+ maxRetries?: number;
16
+ keyPrefix?: string;
17
+ }
18
+
19
+ let redisClient: Redis | null = null;
20
+
21
+ /**
22
+ * Initialize the Redis connection
23
+ */
24
+ export function initRedis(config: RedisConfig): Redis {
25
+ if (redisClient) {
26
+ return redisClient;
27
+ }
28
+
29
+ redisClient = new Redis({
30
+ host: config.host,
31
+ port: config.port,
32
+ password: config.password,
33
+ db: config.db || 0,
34
+ keyPrefix: config.keyPrefix,
35
+ connectTimeout: config.connectTimeout || 10000,
36
+ retryStrategy: (times: number) => {
37
+ const maxRetries = config.maxRetries || 10;
38
+ if (times > maxRetries) {
39
+ logger.error('Redis max retries reached, giving up');
40
+ return null;
41
+ }
42
+ const delay = Math.min(times * 50, 2000);
43
+ return delay;
44
+ },
45
+ ...(config.tls && { tls: {} }),
46
+ });
47
+
48
+ redisClient.on('connect', () => {
49
+ logger.info({ host: config.host, port: config.port }, 'Redis connected');
50
+ });
51
+
52
+ redisClient.on('error', (error: Error) => {
53
+ logger.error({ err: error }, 'Redis error');
54
+ });
55
+
56
+ redisClient.on('close', () => {
57
+ logger.warn('Redis connection closed');
58
+ });
59
+
60
+ redisClient.on('reconnecting', () => {
61
+ logger.info('Redis reconnecting');
62
+ });
63
+
64
+ return redisClient;
65
+ }
66
+
67
+ /**
68
+ * Get the Redis client instance
69
+ * Creates a default connection if not initialized
70
+ */
71
+ export function getRedis(): Redis {
72
+ if (!redisClient) {
73
+ // Initialize with default/env config
74
+ const config: RedisConfig = {
75
+ host: process.env.REDIS_HOST || 'localhost',
76
+ port: parseInt(process.env.REDIS_PORT || '6379', 10),
77
+ password: process.env.REDIS_PASSWORD,
78
+ db: parseInt(process.env.REDIS_DB || '0', 10),
79
+ };
80
+ return initRedis(config);
81
+ }
82
+ return redisClient;
83
+ }
84
+
85
+ /**
86
+ * Close the Redis connection
87
+ */
88
+ export async function closeRedis(): Promise<void> {
89
+ if (redisClient) {
90
+ await redisClient.quit();
91
+ redisClient = null;
92
+ logger.info('Redis connection closed');
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Check if Redis is connected
98
+ */
99
+ export function isRedisConnected(): boolean {
100
+ return redisClient?.status === 'ready';
101
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Mongoose Repositories Export
3
+ * Central export for all Mongoose repositories
4
+ */
5
+
6
+ export * from './user.repository.js';
7
+ export * from './payment.repository.js';
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Mongoose Payment Repository
3
+ * Implements payment operations using Mongoose
4
+ */
5
+
6
+ import {
7
+ PaymentModel,
8
+ SubscriptionModel,
9
+ PlanModel,
10
+ WebhookModel,
11
+ type IPaymentDocument,
12
+ type ISubscriptionDocument,
13
+ type IPlanDocument,
14
+ } from '../../models/mongoose/payment.schema.js';
15
+ import type { IRepository, PaginationOptions, PaginatedResult } from '../../interfaces/index.js';
16
+
17
+ // Payment types matching application format
18
+ export interface Payment {
19
+ id: string;
20
+ userId: string;
21
+ provider: 'stripe' | 'paypal' | 'mobile_money' | 'manual';
22
+ method: 'card' | 'bank_transfer' | 'mobile_money' | 'paypal' | 'other';
23
+ status: 'pending' | 'processing' | 'completed' | 'failed' | 'refunded' | 'cancelled';
24
+ amount: number;
25
+ currency: string;
26
+ description?: string | null;
27
+ providerPaymentId?: string | null;
28
+ providerCustomerId?: string | null;
29
+ metadata?: Record<string, unknown> | null;
30
+ failureReason?: string | null;
31
+ refundedAt?: Date | null;
32
+ completedAt?: Date | null;
33
+ createdAt: Date;
34
+ updatedAt: Date;
35
+ }
36
+
37
+ export interface Subscription {
38
+ id: string;
39
+ userId: string;
40
+ planId: string;
41
+ provider: 'stripe' | 'paypal' | 'mobile_money' | 'manual';
42
+ status: 'active' | 'cancelled' | 'expired' | 'suspended' | 'trialing';
43
+ currentPeriodStart: Date;
44
+ currentPeriodEnd: Date;
45
+ cancelledAt?: Date | null;
46
+ providerSubscriptionId?: string | null;
47
+ metadata?: Record<string, unknown> | null;
48
+ createdAt: Date;
49
+ updatedAt: Date;
50
+ }
51
+
52
+ export interface Plan {
53
+ id: string;
54
+ name: string;
55
+ amount: number;
56
+ currency: string;
57
+ interval: 'day' | 'week' | 'month' | 'year';
58
+ intervalCount: number;
59
+ trialPeriodDays?: number | null;
60
+ active: boolean;
61
+ metadata?: Record<string, unknown> | null;
62
+ createdAt: Date;
63
+ updatedAt: Date;
64
+ }
65
+
66
+ /**
67
+ * Mongoose Payment Repository
68
+ */
69
+ export class MongoosePaymentRepository implements IRepository<Payment> {
70
+ // ==========================================
71
+ // PAYMENT METHODS
72
+ // ==========================================
73
+
74
+ private toPayment(doc: IPaymentDocument): Payment {
75
+ return {
76
+ id: doc._id.toString(),
77
+ userId: doc.userId,
78
+ provider: doc.provider,
79
+ method: doc.method,
80
+ status: doc.status,
81
+ amount: doc.amount,
82
+ currency: doc.currency,
83
+ description: doc.description || null,
84
+ providerPaymentId: doc.providerPaymentId || null,
85
+ providerCustomerId: doc.providerCustomerId || null,
86
+ metadata: doc.metadata || null,
87
+ failureReason: doc.failureReason || null,
88
+ refundedAt: doc.refundedAt || null,
89
+ completedAt: doc.completedAt || null,
90
+ createdAt: doc.createdAt,
91
+ updatedAt: doc.updatedAt,
92
+ };
93
+ }
94
+
95
+ async findById(id: string): Promise<Payment | null> {
96
+ try {
97
+ const payment = await PaymentModel.findById(id);
98
+ return payment ? this.toPayment(payment) : null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ async findOne(filter: Record<string, unknown>): Promise<Payment | null> {
105
+ try {
106
+ const payment = await PaymentModel.findOne(filter);
107
+ return payment ? this.toPayment(payment) : null;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ async findMany(
114
+ filter: Record<string, unknown> = {},
115
+ options: PaginationOptions = {}
116
+ ): Promise<PaginatedResult<Payment>> {
117
+ const page = options.page || 1;
118
+ const limit = options.limit || 10;
119
+ const skip = (page - 1) * limit;
120
+
121
+ const sortBy = options.sortBy || 'createdAt';
122
+ const sortOrder = options.sortOrder === 'asc' ? 1 : -1;
123
+ const sort: Record<string, 1 | -1> = { [sortBy]: sortOrder };
124
+
125
+ const [payments, total] = await Promise.all([
126
+ PaymentModel.find(filter).sort(sort).skip(skip).limit(limit).exec(),
127
+ PaymentModel.countDocuments(filter),
128
+ ]);
129
+
130
+ const totalPages = Math.ceil(total / limit);
131
+
132
+ return {
133
+ data: payments.map((p) => this.toPayment(p)),
134
+ pagination: {
135
+ page,
136
+ limit,
137
+ total,
138
+ totalPages,
139
+ hasNext: page < totalPages,
140
+ hasPrev: page > 1,
141
+ },
142
+ };
143
+ }
144
+
145
+ async create(data: Partial<Payment>): Promise<Payment> {
146
+ const payment = await PaymentModel.create(data);
147
+ return this.toPayment(payment);
148
+ }
149
+
150
+ async update(id: string, data: Partial<Payment>): Promise<Payment | null> {
151
+ try {
152
+ const payment = await PaymentModel.findByIdAndUpdate(
153
+ id,
154
+ { $set: data },
155
+ { new: true, runValidators: true }
156
+ );
157
+ return payment ? this.toPayment(payment) : null;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+
163
+ async delete(id: string): Promise<boolean> {
164
+ try {
165
+ const result = await PaymentModel.findByIdAndDelete(id);
166
+ return result !== null;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ async count(filter: Record<string, unknown> = {}): Promise<number> {
173
+ return PaymentModel.countDocuments(filter);
174
+ }
175
+
176
+ async exists(id: string): Promise<boolean> {
177
+ const count = await PaymentModel.countDocuments({ _id: id });
178
+ return count > 0;
179
+ }
180
+
181
+ /**
182
+ * Find payment by provider payment ID
183
+ */
184
+ async findByProviderPaymentId(providerPaymentId: string): Promise<Payment | null> {
185
+ return this.findOne({ providerPaymentId });
186
+ }
187
+
188
+ /**
189
+ * Find payments by user ID
190
+ */
191
+ async findByUserId(
192
+ userId: string,
193
+ options: PaginationOptions = {}
194
+ ): Promise<PaginatedResult<Payment>> {
195
+ return this.findMany({ userId }, options);
196
+ }
197
+
198
+ /**
199
+ * Update payment status
200
+ */
201
+ async updateStatus(
202
+ id: string,
203
+ status: Payment['status'],
204
+ data?: Partial<Payment>
205
+ ): Promise<Payment | null> {
206
+ const updateData: Partial<Payment> = { status, ...data };
207
+
208
+ if (status === 'completed' && !data?.completedAt) {
209
+ updateData.completedAt = new Date();
210
+ }
211
+ if (status === 'refunded' && !data?.refundedAt) {
212
+ updateData.refundedAt = new Date();
213
+ }
214
+
215
+ return this.update(id, updateData);
216
+ }
217
+
218
+ // ==========================================
219
+ // SUBSCRIPTION METHODS
220
+ // ==========================================
221
+
222
+ private toSubscription(doc: ISubscriptionDocument): Subscription {
223
+ return {
224
+ id: doc._id.toString(),
225
+ userId: doc.userId,
226
+ planId: doc.planId,
227
+ provider: doc.provider,
228
+ status: doc.status,
229
+ currentPeriodStart: doc.currentPeriodStart,
230
+ currentPeriodEnd: doc.currentPeriodEnd,
231
+ cancelledAt: doc.cancelledAt || null,
232
+ providerSubscriptionId: doc.providerSubscriptionId || null,
233
+ metadata: doc.metadata || null,
234
+ createdAt: doc.createdAt,
235
+ updatedAt: doc.updatedAt,
236
+ };
237
+ }
238
+
239
+ async createSubscription(data: Partial<Subscription>): Promise<Subscription> {
240
+ const subscription = await SubscriptionModel.create(data);
241
+ return this.toSubscription(subscription);
242
+ }
243
+
244
+ async findSubscriptionById(id: string): Promise<Subscription | null> {
245
+ try {
246
+ const subscription = await SubscriptionModel.findById(id);
247
+ return subscription ? this.toSubscription(subscription) : null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ async findSubscriptionsByUserId(
254
+ userId: string,
255
+ options: PaginationOptions = {}
256
+ ): Promise<PaginatedResult<Subscription>> {
257
+ const page = options.page || 1;
258
+ const limit = options.limit || 10;
259
+ const skip = (page - 1) * limit;
260
+
261
+ const [subscriptions, total] = await Promise.all([
262
+ SubscriptionModel.find({ userId }).sort({ createdAt: -1 }).skip(skip).limit(limit).exec(),
263
+ SubscriptionModel.countDocuments({ userId }),
264
+ ]);
265
+
266
+ const totalPages = Math.ceil(total / limit);
267
+
268
+ return {
269
+ data: subscriptions.map((s) => this.toSubscription(s)),
270
+ pagination: {
271
+ page,
272
+ limit,
273
+ total,
274
+ totalPages,
275
+ hasNext: page < totalPages,
276
+ hasPrev: page > 1,
277
+ },
278
+ };
279
+ }
280
+
281
+ async updateSubscription(id: string, data: Partial<Subscription>): Promise<Subscription | null> {
282
+ try {
283
+ const subscription = await SubscriptionModel.findByIdAndUpdate(
284
+ id,
285
+ { $set: data },
286
+ { new: true, runValidators: true }
287
+ );
288
+ return subscription ? this.toSubscription(subscription) : null;
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+
294
+ async cancelSubscription(id: string): Promise<Subscription | null> {
295
+ return this.updateSubscription(id, {
296
+ status: 'cancelled',
297
+ cancelledAt: new Date(),
298
+ });
299
+ }
300
+
301
+ // ==========================================
302
+ // PLAN METHODS
303
+ // ==========================================
304
+
305
+ private toPlan(doc: IPlanDocument): Plan {
306
+ return {
307
+ id: doc._id.toString(),
308
+ name: doc.name,
309
+ amount: doc.amount,
310
+ currency: doc.currency,
311
+ interval: doc.interval,
312
+ intervalCount: doc.intervalCount,
313
+ trialPeriodDays: doc.trialPeriodDays || null,
314
+ active: doc.active,
315
+ metadata: doc.metadata || null,
316
+ createdAt: doc.createdAt,
317
+ updatedAt: doc.updatedAt,
318
+ };
319
+ }
320
+
321
+ async createPlan(data: Partial<Plan>): Promise<Plan> {
322
+ const plan = await PlanModel.create(data);
323
+ return this.toPlan(plan);
324
+ }
325
+
326
+ async findPlanById(id: string): Promise<Plan | null> {
327
+ try {
328
+ const plan = await PlanModel.findById(id);
329
+ return plan ? this.toPlan(plan) : null;
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+
335
+ async findActivePlans(): Promise<Plan[]> {
336
+ const plans = await PlanModel.find({ active: true }).sort({ createdAt: -1 });
337
+ return plans.map((p) => this.toPlan(p));
338
+ }
339
+
340
+ async updatePlan(id: string, data: Partial<Plan>): Promise<Plan | null> {
341
+ try {
342
+ const plan = await PlanModel.findByIdAndUpdate(
343
+ id,
344
+ { $set: data },
345
+ { new: true, runValidators: true }
346
+ );
347
+ return plan ? this.toPlan(plan) : null;
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+
353
+ // ==========================================
354
+ // WEBHOOK METHODS
355
+ // ==========================================
356
+
357
+ async storeWebhookEvent(data: {
358
+ provider: 'stripe' | 'paypal' | 'mobile_money' | 'manual';
359
+ type: string;
360
+ data: Record<string, unknown>;
361
+ }): Promise<{ id: string }> {
362
+ const webhook = await WebhookModel.create(data);
363
+ return { id: webhook._id.toString() };
364
+ }
365
+
366
+ async markWebhookProcessed(id: string, error?: string): Promise<boolean> {
367
+ try {
368
+ const result = await WebhookModel.findByIdAndUpdate(id, {
369
+ $set: {
370
+ processed: true,
371
+ processedAt: new Date(),
372
+ ...(error && { error }),
373
+ },
374
+ });
375
+ return result !== null;
376
+ } catch {
377
+ return false;
378
+ }
379
+ }
380
+ }