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.
- package/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common Database Adapter Interface
|
|
3
|
+
* All ORMs must implement this interface to ensure consistency
|
|
4
|
+
*/
|
|
5
|
+
export interface IDatabaseAdapter {
|
|
6
|
+
/**
|
|
7
|
+
* Connect to the database
|
|
8
|
+
* @throws Error if connection fails
|
|
9
|
+
*/
|
|
10
|
+
connect(): Promise<void>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Disconnect from the database
|
|
14
|
+
* Gracefully closes all connections
|
|
15
|
+
*/
|
|
16
|
+
disconnect(): Promise<void>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if database connection is healthy
|
|
20
|
+
* @returns true if connected and responsive
|
|
21
|
+
*/
|
|
22
|
+
healthCheck(): Promise<boolean>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the ORM type
|
|
26
|
+
*/
|
|
27
|
+
getType(): 'prisma' | 'mongoose' | 'sequelize' | 'typeorm';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the database type
|
|
31
|
+
*/
|
|
32
|
+
getDatabaseType(): 'postgresql' | 'mysql' | 'sqlite' | 'mongodb' | 'mariadb';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get raw client instance (use with caution)
|
|
36
|
+
* Type depends on ORM implementation
|
|
37
|
+
*/
|
|
38
|
+
getRawClient(): unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Database configuration
|
|
43
|
+
*/
|
|
44
|
+
export interface DatabaseConfig {
|
|
45
|
+
/** ORM type */
|
|
46
|
+
orm: 'prisma' | 'mongoose' | 'sequelize' | 'typeorm';
|
|
47
|
+
|
|
48
|
+
/** Database type */
|
|
49
|
+
database: 'postgresql' | 'mysql' | 'sqlite' | 'mongodb' | 'mariadb';
|
|
50
|
+
|
|
51
|
+
/** Connection URL (preferred method) */
|
|
52
|
+
url?: string;
|
|
53
|
+
|
|
54
|
+
/** Host (alternative to url) */
|
|
55
|
+
host?: string;
|
|
56
|
+
|
|
57
|
+
/** Port (alternative to url) */
|
|
58
|
+
port?: number;
|
|
59
|
+
|
|
60
|
+
/** Username (alternative to url) */
|
|
61
|
+
username?: string;
|
|
62
|
+
|
|
63
|
+
/** Password (alternative to url) */
|
|
64
|
+
password?: string;
|
|
65
|
+
|
|
66
|
+
/** Database name (alternative to url) */
|
|
67
|
+
databaseName?: string;
|
|
68
|
+
|
|
69
|
+
/** SSL/TLS options */
|
|
70
|
+
ssl?: boolean | Record<string, unknown>;
|
|
71
|
+
|
|
72
|
+
/** Connection pool options */
|
|
73
|
+
pool?: {
|
|
74
|
+
min?: number;
|
|
75
|
+
max?: number;
|
|
76
|
+
idle?: number;
|
|
77
|
+
acquire?: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Logging */
|
|
81
|
+
logging?: boolean | ((query: string) => void);
|
|
82
|
+
|
|
83
|
+
/** Timezone */
|
|
84
|
+
timezone?: string;
|
|
85
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common Repository Interface
|
|
3
|
+
* All repositories must implement this pattern regardless of ORM
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PaginationOptions {
|
|
7
|
+
page?: number;
|
|
8
|
+
limit?: number;
|
|
9
|
+
sortBy?: string;
|
|
10
|
+
sortOrder?: 'asc' | 'desc';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PaginatedResult<T> {
|
|
14
|
+
data: T[];
|
|
15
|
+
pagination: {
|
|
16
|
+
page: number;
|
|
17
|
+
limit: number;
|
|
18
|
+
total: number;
|
|
19
|
+
totalPages: number;
|
|
20
|
+
hasNext: boolean;
|
|
21
|
+
hasPrev: boolean;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Base repository interface that all entity repositories must implement
|
|
27
|
+
*/
|
|
28
|
+
export interface IRepository<T> {
|
|
29
|
+
/**
|
|
30
|
+
* Find entity by ID
|
|
31
|
+
* @param id - Entity ID
|
|
32
|
+
* @returns Entity or null if not found
|
|
33
|
+
*/
|
|
34
|
+
findById(id: string): Promise<T | null>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find many entities with optional filtering and pagination
|
|
38
|
+
* @param filter - Filter criteria (ORM-specific)
|
|
39
|
+
* @param options - Pagination options
|
|
40
|
+
* @returns Paginated results
|
|
41
|
+
*/
|
|
42
|
+
findMany(
|
|
43
|
+
filter?: Record<string, unknown>,
|
|
44
|
+
options?: PaginationOptions
|
|
45
|
+
): Promise<PaginatedResult<T>>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find one entity matching filter
|
|
49
|
+
* @param filter - Filter criteria
|
|
50
|
+
* @returns First matching entity or null
|
|
51
|
+
*/
|
|
52
|
+
findOne(filter: Record<string, unknown>): Promise<T | null>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create new entity
|
|
56
|
+
* @param data - Entity data
|
|
57
|
+
* @returns Created entity
|
|
58
|
+
*/
|
|
59
|
+
create(data: Partial<T>): Promise<T>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Update entity by ID
|
|
63
|
+
* @param id - Entity ID
|
|
64
|
+
* @param data - Update data
|
|
65
|
+
* @returns Updated entity or null if not found
|
|
66
|
+
*/
|
|
67
|
+
update(id: string, data: Partial<T>): Promise<T | null>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Delete entity by ID
|
|
71
|
+
* @param id - Entity ID
|
|
72
|
+
* @returns true if deleted, false if not found
|
|
73
|
+
*/
|
|
74
|
+
delete(id: string): Promise<boolean>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Count entities matching filter
|
|
78
|
+
* @param filter - Filter criteria
|
|
79
|
+
* @returns Number of matching entities
|
|
80
|
+
*/
|
|
81
|
+
count(filter?: Record<string, unknown>): Promise<number>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if entity exists
|
|
85
|
+
* @param id - Entity ID
|
|
86
|
+
* @returns true if exists
|
|
87
|
+
*/
|
|
88
|
+
exists(id: string): Promise<boolean>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Transaction interface for atomic operations
|
|
93
|
+
*/
|
|
94
|
+
export interface ITransaction {
|
|
95
|
+
/**
|
|
96
|
+
* Commit the transaction
|
|
97
|
+
*/
|
|
98
|
+
commit(): Promise<void>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Rollback the transaction
|
|
102
|
+
*/
|
|
103
|
+
rollback(): Promise<void>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Execute operation within transaction
|
|
107
|
+
* @param operation - Async operation to execute
|
|
108
|
+
* @returns Operation result
|
|
109
|
+
*/
|
|
110
|
+
execute<R>(operation: () => Promise<R>): Promise<R>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Transaction manager interface
|
|
115
|
+
*/
|
|
116
|
+
export interface ITransactionManager {
|
|
117
|
+
/**
|
|
118
|
+
* Begin a new transaction
|
|
119
|
+
* @returns Transaction instance
|
|
120
|
+
*/
|
|
121
|
+
begin(): Promise<ITransaction>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Execute operations in a transaction (auto commit/rollback)
|
|
125
|
+
* @param callback - Operations to execute
|
|
126
|
+
* @returns Callback result
|
|
127
|
+
*/
|
|
128
|
+
transaction<R>(callback: (tx: ITransaction) => Promise<R>): Promise<R>;
|
|
129
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mongoose Payment Schemas
|
|
3
|
+
* MongoDB schemas for Payment, Subscription, Plan, and Webhook entities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Schema, model, type Document } from 'mongoose';
|
|
7
|
+
|
|
8
|
+
// ==========================================
|
|
9
|
+
// PAYMENT SCHEMA
|
|
10
|
+
// ==========================================
|
|
11
|
+
|
|
12
|
+
export interface IPaymentDocument extends Document {
|
|
13
|
+
userId: string;
|
|
14
|
+
provider: 'stripe' | 'paypal' | 'mobile_money' | 'manual';
|
|
15
|
+
method: 'card' | 'bank_transfer' | 'mobile_money' | 'paypal' | 'other';
|
|
16
|
+
status: 'pending' | 'processing' | 'completed' | 'failed' | 'refunded' | 'cancelled';
|
|
17
|
+
amount: number;
|
|
18
|
+
currency: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
providerPaymentId?: string;
|
|
21
|
+
providerCustomerId?: string;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
failureReason?: string;
|
|
24
|
+
refundedAt?: Date;
|
|
25
|
+
completedAt?: Date;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const paymentSchema = new Schema<IPaymentDocument>(
|
|
31
|
+
{
|
|
32
|
+
userId: {
|
|
33
|
+
type: String,
|
|
34
|
+
required: [true, 'User ID is required'],
|
|
35
|
+
index: true,
|
|
36
|
+
},
|
|
37
|
+
provider: {
|
|
38
|
+
type: String,
|
|
39
|
+
enum: ['stripe', 'paypal', 'mobile_money', 'manual'],
|
|
40
|
+
required: [true, 'Provider is required'],
|
|
41
|
+
default: 'manual',
|
|
42
|
+
},
|
|
43
|
+
method: {
|
|
44
|
+
type: String,
|
|
45
|
+
enum: ['card', 'bank_transfer', 'mobile_money', 'paypal', 'other'],
|
|
46
|
+
required: [true, 'Payment method is required'],
|
|
47
|
+
default: 'card',
|
|
48
|
+
},
|
|
49
|
+
status: {
|
|
50
|
+
type: String,
|
|
51
|
+
enum: ['pending', 'processing', 'completed', 'failed', 'refunded', 'cancelled'],
|
|
52
|
+
required: [true, 'Status is required'],
|
|
53
|
+
default: 'pending',
|
|
54
|
+
index: true,
|
|
55
|
+
},
|
|
56
|
+
amount: {
|
|
57
|
+
type: Number,
|
|
58
|
+
required: [true, 'Amount is required'],
|
|
59
|
+
min: [0, 'Amount must be positive'],
|
|
60
|
+
},
|
|
61
|
+
currency: {
|
|
62
|
+
type: String,
|
|
63
|
+
required: [true, 'Currency is required'],
|
|
64
|
+
default: 'USD',
|
|
65
|
+
uppercase: true,
|
|
66
|
+
length: 3,
|
|
67
|
+
},
|
|
68
|
+
description: {
|
|
69
|
+
type: String,
|
|
70
|
+
maxlength: [500, 'Description cannot exceed 500 characters'],
|
|
71
|
+
},
|
|
72
|
+
providerPaymentId: {
|
|
73
|
+
type: String,
|
|
74
|
+
index: true,
|
|
75
|
+
sparse: true,
|
|
76
|
+
},
|
|
77
|
+
providerCustomerId: {
|
|
78
|
+
type: String,
|
|
79
|
+
index: true,
|
|
80
|
+
sparse: true,
|
|
81
|
+
},
|
|
82
|
+
metadata: {
|
|
83
|
+
type: Schema.Types.Mixed,
|
|
84
|
+
default: null,
|
|
85
|
+
},
|
|
86
|
+
failureReason: {
|
|
87
|
+
type: String,
|
|
88
|
+
maxlength: [500, 'Failure reason cannot exceed 500 characters'],
|
|
89
|
+
},
|
|
90
|
+
refundedAt: {
|
|
91
|
+
type: Date,
|
|
92
|
+
default: null,
|
|
93
|
+
},
|
|
94
|
+
completedAt: {
|
|
95
|
+
type: Date,
|
|
96
|
+
default: null,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
timestamps: true,
|
|
101
|
+
collection: 'payments',
|
|
102
|
+
toJSON: {
|
|
103
|
+
virtuals: true,
|
|
104
|
+
transform: (_doc, ret): Record<string, unknown> => {
|
|
105
|
+
const { _id, ...rest } = ret;
|
|
106
|
+
return { id: _id.toString(), ...rest };
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Indexes
|
|
113
|
+
paymentSchema.index({ userId: 1, createdAt: -1 });
|
|
114
|
+
paymentSchema.index({ provider: 1, providerPaymentId: 1 });
|
|
115
|
+
paymentSchema.index({ status: 1, createdAt: -1 });
|
|
116
|
+
|
|
117
|
+
export const PaymentModel = model<IPaymentDocument>('Payment', paymentSchema);
|
|
118
|
+
|
|
119
|
+
// ==========================================
|
|
120
|
+
// SUBSCRIPTION SCHEMA
|
|
121
|
+
// ==========================================
|
|
122
|
+
|
|
123
|
+
export interface ISubscriptionDocument extends Document {
|
|
124
|
+
userId: string;
|
|
125
|
+
planId: string;
|
|
126
|
+
provider: 'stripe' | 'paypal' | 'mobile_money' | 'manual';
|
|
127
|
+
status: 'active' | 'cancelled' | 'expired' | 'suspended' | 'trialing';
|
|
128
|
+
currentPeriodStart: Date;
|
|
129
|
+
currentPeriodEnd: Date;
|
|
130
|
+
cancelledAt?: Date;
|
|
131
|
+
providerSubscriptionId?: string;
|
|
132
|
+
metadata?: Record<string, unknown>;
|
|
133
|
+
createdAt: Date;
|
|
134
|
+
updatedAt: Date;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const subscriptionSchema = new Schema<ISubscriptionDocument>(
|
|
138
|
+
{
|
|
139
|
+
userId: {
|
|
140
|
+
type: String,
|
|
141
|
+
required: [true, 'User ID is required'],
|
|
142
|
+
index: true,
|
|
143
|
+
},
|
|
144
|
+
planId: {
|
|
145
|
+
type: String,
|
|
146
|
+
required: [true, 'Plan ID is required'],
|
|
147
|
+
index: true,
|
|
148
|
+
},
|
|
149
|
+
provider: {
|
|
150
|
+
type: String,
|
|
151
|
+
enum: ['stripe', 'paypal', 'mobile_money', 'manual'],
|
|
152
|
+
required: [true, 'Provider is required'],
|
|
153
|
+
default: 'manual',
|
|
154
|
+
},
|
|
155
|
+
status: {
|
|
156
|
+
type: String,
|
|
157
|
+
enum: ['active', 'cancelled', 'expired', 'suspended', 'trialing'],
|
|
158
|
+
required: [true, 'Status is required'],
|
|
159
|
+
default: 'active',
|
|
160
|
+
index: true,
|
|
161
|
+
},
|
|
162
|
+
currentPeriodStart: {
|
|
163
|
+
type: Date,
|
|
164
|
+
required: [true, 'Current period start is required'],
|
|
165
|
+
},
|
|
166
|
+
currentPeriodEnd: {
|
|
167
|
+
type: Date,
|
|
168
|
+
required: [true, 'Current period end is required'],
|
|
169
|
+
},
|
|
170
|
+
cancelledAt: {
|
|
171
|
+
type: Date,
|
|
172
|
+
default: null,
|
|
173
|
+
},
|
|
174
|
+
providerSubscriptionId: {
|
|
175
|
+
type: String,
|
|
176
|
+
index: true,
|
|
177
|
+
sparse: true,
|
|
178
|
+
},
|
|
179
|
+
metadata: {
|
|
180
|
+
type: Schema.Types.Mixed,
|
|
181
|
+
default: null,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
timestamps: true,
|
|
186
|
+
collection: 'subscriptions',
|
|
187
|
+
toJSON: {
|
|
188
|
+
virtuals: true,
|
|
189
|
+
transform: (_doc, ret): Record<string, unknown> => {
|
|
190
|
+
const { _id, ...rest } = ret;
|
|
191
|
+
return { id: _id.toString(), ...rest };
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Indexes
|
|
198
|
+
subscriptionSchema.index({ userId: 1, status: 1 });
|
|
199
|
+
subscriptionSchema.index({ currentPeriodEnd: 1, status: 1 });
|
|
200
|
+
|
|
201
|
+
export const SubscriptionModel = model<ISubscriptionDocument>('Subscription', subscriptionSchema);
|
|
202
|
+
|
|
203
|
+
// ==========================================
|
|
204
|
+
// PLAN SCHEMA
|
|
205
|
+
// ==========================================
|
|
206
|
+
|
|
207
|
+
export interface IPlanDocument extends Document {
|
|
208
|
+
name: string;
|
|
209
|
+
amount: number;
|
|
210
|
+
currency: string;
|
|
211
|
+
interval: 'day' | 'week' | 'month' | 'year';
|
|
212
|
+
intervalCount: number;
|
|
213
|
+
trialPeriodDays?: number;
|
|
214
|
+
active: boolean;
|
|
215
|
+
metadata?: Record<string, unknown>;
|
|
216
|
+
createdAt: Date;
|
|
217
|
+
updatedAt: Date;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const planSchema = new Schema<IPlanDocument>(
|
|
221
|
+
{
|
|
222
|
+
name: {
|
|
223
|
+
type: String,
|
|
224
|
+
required: [true, 'Plan name is required'],
|
|
225
|
+
trim: true,
|
|
226
|
+
maxlength: [100, 'Plan name cannot exceed 100 characters'],
|
|
227
|
+
},
|
|
228
|
+
amount: {
|
|
229
|
+
type: Number,
|
|
230
|
+
required: [true, 'Amount is required'],
|
|
231
|
+
min: [0, 'Amount must be positive'],
|
|
232
|
+
},
|
|
233
|
+
currency: {
|
|
234
|
+
type: String,
|
|
235
|
+
required: [true, 'Currency is required'],
|
|
236
|
+
default: 'USD',
|
|
237
|
+
uppercase: true,
|
|
238
|
+
length: 3,
|
|
239
|
+
},
|
|
240
|
+
interval: {
|
|
241
|
+
type: String,
|
|
242
|
+
enum: ['day', 'week', 'month', 'year'],
|
|
243
|
+
required: [true, 'Interval is required'],
|
|
244
|
+
default: 'month',
|
|
245
|
+
},
|
|
246
|
+
intervalCount: {
|
|
247
|
+
type: Number,
|
|
248
|
+
required: [true, 'Interval count is required'],
|
|
249
|
+
default: 1,
|
|
250
|
+
min: [1, 'Interval count must be at least 1'],
|
|
251
|
+
},
|
|
252
|
+
trialPeriodDays: {
|
|
253
|
+
type: Number,
|
|
254
|
+
min: [0, 'Trial period must be positive'],
|
|
255
|
+
default: null,
|
|
256
|
+
},
|
|
257
|
+
active: {
|
|
258
|
+
type: Boolean,
|
|
259
|
+
default: true,
|
|
260
|
+
index: true,
|
|
261
|
+
},
|
|
262
|
+
metadata: {
|
|
263
|
+
type: Schema.Types.Mixed,
|
|
264
|
+
default: null,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
timestamps: true,
|
|
269
|
+
collection: 'plans',
|
|
270
|
+
toJSON: {
|
|
271
|
+
virtuals: true,
|
|
272
|
+
transform: (_doc, ret): Record<string, unknown> => {
|
|
273
|
+
const { _id, ...rest } = ret;
|
|
274
|
+
return { id: _id.toString(), ...rest };
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Indexes
|
|
281
|
+
planSchema.index({ active: 1, createdAt: -1 });
|
|
282
|
+
|
|
283
|
+
export const PlanModel = model<IPlanDocument>('Plan', planSchema);
|
|
284
|
+
|
|
285
|
+
// ==========================================
|
|
286
|
+
// WEBHOOK SCHEMA
|
|
287
|
+
// ==========================================
|
|
288
|
+
|
|
289
|
+
export interface IWebhookDocument extends Document {
|
|
290
|
+
provider: 'stripe' | 'paypal' | 'mobile_money' | 'manual';
|
|
291
|
+
type: string;
|
|
292
|
+
data: Record<string, unknown>;
|
|
293
|
+
processed: boolean;
|
|
294
|
+
processedAt?: Date;
|
|
295
|
+
error?: string;
|
|
296
|
+
createdAt: Date;
|
|
297
|
+
updatedAt: Date;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const webhookSchema = new Schema<IWebhookDocument>(
|
|
301
|
+
{
|
|
302
|
+
provider: {
|
|
303
|
+
type: String,
|
|
304
|
+
enum: ['stripe', 'paypal', 'mobile_money', 'manual'],
|
|
305
|
+
required: [true, 'Provider is required'],
|
|
306
|
+
},
|
|
307
|
+
type: {
|
|
308
|
+
type: String,
|
|
309
|
+
required: [true, 'Webhook type is required'],
|
|
310
|
+
index: true,
|
|
311
|
+
},
|
|
312
|
+
data: {
|
|
313
|
+
type: Schema.Types.Mixed,
|
|
314
|
+
required: [true, 'Webhook data is required'],
|
|
315
|
+
},
|
|
316
|
+
processed: {
|
|
317
|
+
type: Boolean,
|
|
318
|
+
default: false,
|
|
319
|
+
index: true,
|
|
320
|
+
},
|
|
321
|
+
processedAt: {
|
|
322
|
+
type: Date,
|
|
323
|
+
default: null,
|
|
324
|
+
},
|
|
325
|
+
error: {
|
|
326
|
+
type: String,
|
|
327
|
+
maxlength: [1000, 'Error message cannot exceed 1000 characters'],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
timestamps: true,
|
|
332
|
+
collection: 'payment_webhooks',
|
|
333
|
+
toJSON: {
|
|
334
|
+
virtuals: true,
|
|
335
|
+
transform: (_doc, ret): Record<string, unknown> => {
|
|
336
|
+
const { _id, ...rest } = ret;
|
|
337
|
+
return { id: _id.toString(), ...rest };
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Indexes
|
|
344
|
+
webhookSchema.index({ provider: 1, processed: 1 });
|
|
345
|
+
webhookSchema.index({ createdAt: -1 });
|
|
346
|
+
|
|
347
|
+
export const WebhookModel = model<IWebhookDocument>('PaymentWebhook', webhookSchema);
|