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,637 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2
|
+
import { prisma } from '../../src/database/prisma.js';
|
|
3
|
+
import { PaymentRepository } from '../../src/modules/payment/payment.repository.js';
|
|
4
|
+
import { UserRepository } from '../../src/modules/user/user.repository.js';
|
|
5
|
+
|
|
6
|
+
describe('PaymentRepository - Prisma Integration', () => {
|
|
7
|
+
let paymentRepo: PaymentRepository;
|
|
8
|
+
let userRepo: UserRepository;
|
|
9
|
+
let testUserId: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
paymentRepo = new PaymentRepository();
|
|
13
|
+
userRepo = new UserRepository();
|
|
14
|
+
|
|
15
|
+
// Create a test user
|
|
16
|
+
const user = await userRepo.create({
|
|
17
|
+
email: 'payment-test@example.com',
|
|
18
|
+
password: 'hashedpassword123',
|
|
19
|
+
name: 'Payment Test User',
|
|
20
|
+
});
|
|
21
|
+
testUserId = user.id;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterAll(async () => {
|
|
25
|
+
await userRepo.clear();
|
|
26
|
+
await prisma.$disconnect();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
// Clean up before each test
|
|
31
|
+
await paymentRepo.clearPayments();
|
|
32
|
+
await paymentRepo.clearSubscriptions();
|
|
33
|
+
await paymentRepo.clearPlans();
|
|
34
|
+
await paymentRepo.clearWebhooks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ==========================================
|
|
38
|
+
// PAYMENT TESTS
|
|
39
|
+
// ==========================================
|
|
40
|
+
|
|
41
|
+
describe('Payment Operations', () => {
|
|
42
|
+
it('should create a new payment', async () => {
|
|
43
|
+
const payment = await paymentRepo.createPayment({
|
|
44
|
+
userId: testUserId,
|
|
45
|
+
provider: 'stripe',
|
|
46
|
+
method: 'card',
|
|
47
|
+
amount: 99.99,
|
|
48
|
+
currency: 'USD',
|
|
49
|
+
description: 'Test payment',
|
|
50
|
+
metadata: { orderId: '12345' },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(payment).toBeDefined();
|
|
54
|
+
expect(payment.id).toBeDefined();
|
|
55
|
+
expect(payment.userId).toBe(testUserId);
|
|
56
|
+
expect(payment.provider).toBe('stripe');
|
|
57
|
+
expect(payment.method).toBe('card');
|
|
58
|
+
expect(payment.status).toBe('pending');
|
|
59
|
+
expect(payment.amount).toBe(99.99);
|
|
60
|
+
expect(payment.currency).toBe('USD');
|
|
61
|
+
expect(payment.createdAt).toBeInstanceOf(Date);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should find payment by ID', async () => {
|
|
65
|
+
const created = await paymentRepo.createPayment({
|
|
66
|
+
userId: testUserId,
|
|
67
|
+
provider: 'paypal',
|
|
68
|
+
method: 'paypal',
|
|
69
|
+
amount: 49.99,
|
|
70
|
+
currency: 'EUR',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const found = await paymentRepo.findPaymentById(created.id);
|
|
74
|
+
expect(found).toBeDefined();
|
|
75
|
+
expect(found?.id).toBe(created.id);
|
|
76
|
+
expect(found?.amount).toBe(49.99);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return null for non-existent payment ID', async () => {
|
|
80
|
+
const found = await paymentRepo.findPaymentById('non-existent-id');
|
|
81
|
+
expect(found).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should find payment by provider payment ID', async () => {
|
|
85
|
+
const providerPaymentId = 'pi_test_12345';
|
|
86
|
+
const created = await paymentRepo.createPayment({
|
|
87
|
+
userId: testUserId,
|
|
88
|
+
provider: 'stripe',
|
|
89
|
+
method: 'card',
|
|
90
|
+
amount: 99.99,
|
|
91
|
+
currency: 'USD',
|
|
92
|
+
providerPaymentId,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const found = await paymentRepo.findPaymentByProviderPaymentId(providerPaymentId);
|
|
96
|
+
expect(found).toBeDefined();
|
|
97
|
+
expect(found?.id).toBe(created.id);
|
|
98
|
+
expect(found?.providerPaymentId).toBe(providerPaymentId);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should find user payments with pagination', async () => {
|
|
102
|
+
// Create multiple payments
|
|
103
|
+
await Promise.all([
|
|
104
|
+
paymentRepo.createPayment({
|
|
105
|
+
userId: testUserId,
|
|
106
|
+
provider: 'stripe',
|
|
107
|
+
method: 'card',
|
|
108
|
+
amount: 10,
|
|
109
|
+
currency: 'USD',
|
|
110
|
+
}),
|
|
111
|
+
paymentRepo.createPayment({
|
|
112
|
+
userId: testUserId,
|
|
113
|
+
provider: 'paypal',
|
|
114
|
+
method: 'paypal',
|
|
115
|
+
amount: 20,
|
|
116
|
+
currency: 'USD',
|
|
117
|
+
}),
|
|
118
|
+
paymentRepo.createPayment({
|
|
119
|
+
userId: testUserId,
|
|
120
|
+
provider: 'stripe',
|
|
121
|
+
method: 'card',
|
|
122
|
+
amount: 30,
|
|
123
|
+
currency: 'USD',
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const result = await paymentRepo.findUserPayments(testUserId, { page: 1, limit: 10 });
|
|
128
|
+
|
|
129
|
+
expect(result.data).toHaveLength(3);
|
|
130
|
+
expect(result.meta.total).toBe(3);
|
|
131
|
+
expect(result.meta.page).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should update payment status to completed', async () => {
|
|
135
|
+
const payment = await paymentRepo.createPayment({
|
|
136
|
+
userId: testUserId,
|
|
137
|
+
provider: 'stripe',
|
|
138
|
+
method: 'card',
|
|
139
|
+
amount: 99.99,
|
|
140
|
+
currency: 'USD',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const paidAt = new Date();
|
|
144
|
+
const updated = await paymentRepo.updatePaymentStatus(payment.id, 'completed', {
|
|
145
|
+
paidAt,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(updated).toBeDefined();
|
|
149
|
+
expect(updated?.status).toBe('completed');
|
|
150
|
+
expect(updated?.paidAt).toBeInstanceOf(Date);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should update payment status to failed', async () => {
|
|
154
|
+
const payment = await paymentRepo.createPayment({
|
|
155
|
+
userId: testUserId,
|
|
156
|
+
provider: 'stripe',
|
|
157
|
+
method: 'card',
|
|
158
|
+
amount: 99.99,
|
|
159
|
+
currency: 'USD',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const updated = await paymentRepo.updatePaymentStatus(payment.id, 'failed', {
|
|
163
|
+
failureReason: 'Insufficient funds',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(updated).toBeDefined();
|
|
167
|
+
expect(updated?.status).toBe('failed');
|
|
168
|
+
expect(updated?.failureReason).toBe('Insufficient funds');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should update payment status to refunded', async () => {
|
|
172
|
+
const payment = await paymentRepo.createPayment({
|
|
173
|
+
userId: testUserId,
|
|
174
|
+
provider: 'stripe',
|
|
175
|
+
method: 'card',
|
|
176
|
+
amount: 99.99,
|
|
177
|
+
currency: 'USD',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await paymentRepo.updatePaymentStatus(payment.id, 'completed', { paidAt: new Date() });
|
|
181
|
+
|
|
182
|
+
const updated = await paymentRepo.updatePaymentStatus(payment.id, 'refunded', {
|
|
183
|
+
refundedAmount: 99.99,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(updated).toBeDefined();
|
|
187
|
+
expect(updated?.status).toBe('refunded');
|
|
188
|
+
expect(updated?.refundedAmount).toBe(99.99);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should delete payment', async () => {
|
|
192
|
+
const payment = await paymentRepo.createPayment({
|
|
193
|
+
userId: testUserId,
|
|
194
|
+
provider: 'stripe',
|
|
195
|
+
method: 'card',
|
|
196
|
+
amount: 99.99,
|
|
197
|
+
currency: 'USD',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const deleted = await paymentRepo.deletePayment(payment.id);
|
|
201
|
+
expect(deleted).toBe(true);
|
|
202
|
+
|
|
203
|
+
const found = await paymentRepo.findPaymentById(payment.id);
|
|
204
|
+
expect(found).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should count payments with filters', async () => {
|
|
208
|
+
await Promise.all([
|
|
209
|
+
paymentRepo.createPayment({
|
|
210
|
+
userId: testUserId,
|
|
211
|
+
provider: 'stripe',
|
|
212
|
+
method: 'card',
|
|
213
|
+
amount: 10,
|
|
214
|
+
currency: 'USD',
|
|
215
|
+
}),
|
|
216
|
+
paymentRepo.createPayment({
|
|
217
|
+
userId: testUserId,
|
|
218
|
+
provider: 'paypal',
|
|
219
|
+
method: 'paypal',
|
|
220
|
+
amount: 20,
|
|
221
|
+
currency: 'USD',
|
|
222
|
+
}),
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const allCount = await paymentRepo.countPayments();
|
|
226
|
+
expect(allCount).toBe(2);
|
|
227
|
+
|
|
228
|
+
const stripeCount = await paymentRepo.countPayments({ provider: 'stripe' });
|
|
229
|
+
expect(stripeCount).toBe(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should filter payments by status', async () => {
|
|
233
|
+
const payment1 = await paymentRepo.createPayment({
|
|
234
|
+
userId: testUserId,
|
|
235
|
+
provider: 'stripe',
|
|
236
|
+
method: 'card',
|
|
237
|
+
amount: 10,
|
|
238
|
+
currency: 'USD',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await paymentRepo.createPayment({
|
|
242
|
+
userId: testUserId,
|
|
243
|
+
provider: 'stripe',
|
|
244
|
+
method: 'card',
|
|
245
|
+
amount: 20,
|
|
246
|
+
currency: 'USD',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await paymentRepo.updatePaymentStatus(payment1.id, 'completed', { paidAt: new Date() });
|
|
250
|
+
|
|
251
|
+
const result = await paymentRepo.findPayments(
|
|
252
|
+
{ page: 1, limit: 10 },
|
|
253
|
+
{ status: 'completed' }
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(result.data).toHaveLength(1);
|
|
257
|
+
expect(result.data[0]?.status).toBe('completed');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ==========================================
|
|
262
|
+
// SUBSCRIPTION TESTS
|
|
263
|
+
// ==========================================
|
|
264
|
+
|
|
265
|
+
describe('Subscription Operations', () => {
|
|
266
|
+
let testPlanId: string;
|
|
267
|
+
|
|
268
|
+
beforeEach(async () => {
|
|
269
|
+
const plan = await paymentRepo.createPlan({
|
|
270
|
+
name: 'Test Plan',
|
|
271
|
+
amount: 9.99,
|
|
272
|
+
currency: 'USD',
|
|
273
|
+
interval: 'month',
|
|
274
|
+
intervalCount: 1,
|
|
275
|
+
});
|
|
276
|
+
testPlanId = plan.id;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should create a new subscription', async () => {
|
|
280
|
+
const currentPeriodStart = new Date();
|
|
281
|
+
const currentPeriodEnd = new Date();
|
|
282
|
+
currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 1);
|
|
283
|
+
|
|
284
|
+
const subscription = await paymentRepo.createSubscription({
|
|
285
|
+
userId: testUserId,
|
|
286
|
+
planId: testPlanId,
|
|
287
|
+
provider: 'stripe',
|
|
288
|
+
currentPeriodStart,
|
|
289
|
+
currentPeriodEnd,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(subscription).toBeDefined();
|
|
293
|
+
expect(subscription.id).toBeDefined();
|
|
294
|
+
expect(subscription.userId).toBe(testUserId);
|
|
295
|
+
expect(subscription.planId).toBe(testPlanId);
|
|
296
|
+
expect(subscription.status).toBe('active');
|
|
297
|
+
expect(subscription.cancelAtPeriodEnd).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should find subscription by ID', async () => {
|
|
301
|
+
const currentPeriodStart = new Date();
|
|
302
|
+
const currentPeriodEnd = new Date();
|
|
303
|
+
currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 1);
|
|
304
|
+
|
|
305
|
+
const created = await paymentRepo.createSubscription({
|
|
306
|
+
userId: testUserId,
|
|
307
|
+
planId: testPlanId,
|
|
308
|
+
provider: 'stripe',
|
|
309
|
+
currentPeriodStart,
|
|
310
|
+
currentPeriodEnd,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const found = await paymentRepo.findSubscriptionById(created.id);
|
|
314
|
+
expect(found).toBeDefined();
|
|
315
|
+
expect(found?.id).toBe(created.id);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should find user subscriptions', async () => {
|
|
319
|
+
const currentPeriodStart = new Date();
|
|
320
|
+
const currentPeriodEnd = new Date();
|
|
321
|
+
currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 1);
|
|
322
|
+
|
|
323
|
+
await Promise.all([
|
|
324
|
+
paymentRepo.createSubscription({
|
|
325
|
+
userId: testUserId,
|
|
326
|
+
planId: testPlanId,
|
|
327
|
+
provider: 'stripe',
|
|
328
|
+
currentPeriodStart,
|
|
329
|
+
currentPeriodEnd,
|
|
330
|
+
}),
|
|
331
|
+
paymentRepo.createSubscription({
|
|
332
|
+
userId: testUserId,
|
|
333
|
+
planId: testPlanId,
|
|
334
|
+
provider: 'stripe',
|
|
335
|
+
currentPeriodStart,
|
|
336
|
+
currentPeriodEnd,
|
|
337
|
+
}),
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
const subscriptions = await paymentRepo.findUserSubscriptions(testUserId);
|
|
341
|
+
expect(subscriptions).toHaveLength(2);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should update subscription status', async () => {
|
|
345
|
+
const currentPeriodStart = new Date();
|
|
346
|
+
const currentPeriodEnd = new Date();
|
|
347
|
+
currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 1);
|
|
348
|
+
|
|
349
|
+
const subscription = await paymentRepo.createSubscription({
|
|
350
|
+
userId: testUserId,
|
|
351
|
+
planId: testPlanId,
|
|
352
|
+
provider: 'stripe',
|
|
353
|
+
currentPeriodStart,
|
|
354
|
+
currentPeriodEnd,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const updated = await paymentRepo.updateSubscriptionStatus(subscription.id, 'past_due');
|
|
358
|
+
expect(updated).toBeDefined();
|
|
359
|
+
expect(updated?.status).toBe('past_due');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should cancel subscription', async () => {
|
|
363
|
+
const currentPeriodStart = new Date();
|
|
364
|
+
const currentPeriodEnd = new Date();
|
|
365
|
+
currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 1);
|
|
366
|
+
|
|
367
|
+
const subscription = await paymentRepo.createSubscription({
|
|
368
|
+
userId: testUserId,
|
|
369
|
+
planId: testPlanId,
|
|
370
|
+
provider: 'stripe',
|
|
371
|
+
currentPeriodStart,
|
|
372
|
+
currentPeriodEnd,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const cancelled = await paymentRepo.cancelSubscription(subscription.id);
|
|
376
|
+
expect(cancelled).toBeDefined();
|
|
377
|
+
expect(cancelled?.cancelAtPeriodEnd).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should delete subscription', async () => {
|
|
381
|
+
const currentPeriodStart = new Date();
|
|
382
|
+
const currentPeriodEnd = new Date();
|
|
383
|
+
currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 1);
|
|
384
|
+
|
|
385
|
+
const subscription = await paymentRepo.createSubscription({
|
|
386
|
+
userId: testUserId,
|
|
387
|
+
planId: testPlanId,
|
|
388
|
+
provider: 'stripe',
|
|
389
|
+
currentPeriodStart,
|
|
390
|
+
currentPeriodEnd,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const deleted = await paymentRepo.deleteSubscription(subscription.id);
|
|
394
|
+
expect(deleted).toBe(true);
|
|
395
|
+
|
|
396
|
+
const found = await paymentRepo.findSubscriptionById(subscription.id);
|
|
397
|
+
expect(found).toBeNull();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ==========================================
|
|
402
|
+
// PLAN TESTS
|
|
403
|
+
// ==========================================
|
|
404
|
+
|
|
405
|
+
describe('Plan Operations', () => {
|
|
406
|
+
it('should create a new plan', async () => {
|
|
407
|
+
const plan = await paymentRepo.createPlan({
|
|
408
|
+
name: 'Premium Plan',
|
|
409
|
+
description: 'Premium features',
|
|
410
|
+
amount: 29.99,
|
|
411
|
+
currency: 'USD',
|
|
412
|
+
interval: 'month',
|
|
413
|
+
intervalCount: 1,
|
|
414
|
+
trialDays: 7,
|
|
415
|
+
features: ['Feature 1', 'Feature 2'],
|
|
416
|
+
metadata: { priority: 'high' },
|
|
417
|
+
active: true,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(plan).toBeDefined();
|
|
421
|
+
expect(plan.id).toBeDefined();
|
|
422
|
+
expect(plan.name).toBe('Premium Plan');
|
|
423
|
+
expect(plan.amount).toBe(29.99);
|
|
424
|
+
expect(plan.interval).toBe('month');
|
|
425
|
+
expect(plan.trialDays).toBe(7);
|
|
426
|
+
expect(plan.active).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should find plan by ID', async () => {
|
|
430
|
+
const created = await paymentRepo.createPlan({
|
|
431
|
+
name: 'Basic Plan',
|
|
432
|
+
amount: 9.99,
|
|
433
|
+
currency: 'USD',
|
|
434
|
+
interval: 'month',
|
|
435
|
+
intervalCount: 1,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const found = await paymentRepo.findPlanById(created.id);
|
|
439
|
+
expect(found).toBeDefined();
|
|
440
|
+
expect(found?.id).toBe(created.id);
|
|
441
|
+
expect(found?.name).toBe('Basic Plan');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should find all active plans', async () => {
|
|
445
|
+
await Promise.all([
|
|
446
|
+
paymentRepo.createPlan({
|
|
447
|
+
name: 'Plan 1',
|
|
448
|
+
amount: 9.99,
|
|
449
|
+
currency: 'USD',
|
|
450
|
+
interval: 'month',
|
|
451
|
+
intervalCount: 1,
|
|
452
|
+
active: true,
|
|
453
|
+
}),
|
|
454
|
+
paymentRepo.createPlan({
|
|
455
|
+
name: 'Plan 2',
|
|
456
|
+
amount: 19.99,
|
|
457
|
+
currency: 'USD',
|
|
458
|
+
interval: 'month',
|
|
459
|
+
intervalCount: 1,
|
|
460
|
+
active: true,
|
|
461
|
+
}),
|
|
462
|
+
paymentRepo.createPlan({
|
|
463
|
+
name: 'Plan 3',
|
|
464
|
+
amount: 29.99,
|
|
465
|
+
currency: 'USD',
|
|
466
|
+
interval: 'month',
|
|
467
|
+
intervalCount: 1,
|
|
468
|
+
active: false,
|
|
469
|
+
}),
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
const activePlans = await paymentRepo.findActivePlans();
|
|
473
|
+
expect(activePlans).toHaveLength(2);
|
|
474
|
+
expect(activePlans.every((p) => p.active)).toBe(true);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should update plan', async () => {
|
|
478
|
+
const plan = await paymentRepo.createPlan({
|
|
479
|
+
name: 'Original Plan',
|
|
480
|
+
amount: 9.99,
|
|
481
|
+
currency: 'USD',
|
|
482
|
+
interval: 'month',
|
|
483
|
+
intervalCount: 1,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const updated = await paymentRepo.updatePlan(plan.id, {
|
|
487
|
+
name: 'Updated Plan',
|
|
488
|
+
amount: 14.99,
|
|
489
|
+
active: false,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
expect(updated).toBeDefined();
|
|
493
|
+
expect(updated?.name).toBe('Updated Plan');
|
|
494
|
+
expect(updated?.amount).toBe(14.99);
|
|
495
|
+
expect(updated?.active).toBe(false);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should delete plan', async () => {
|
|
499
|
+
const plan = await paymentRepo.createPlan({
|
|
500
|
+
name: 'To Delete',
|
|
501
|
+
amount: 9.99,
|
|
502
|
+
currency: 'USD',
|
|
503
|
+
interval: 'month',
|
|
504
|
+
intervalCount: 1,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const deleted = await paymentRepo.deletePlan(plan.id);
|
|
508
|
+
expect(deleted).toBe(true);
|
|
509
|
+
|
|
510
|
+
const found = await paymentRepo.findPlanById(plan.id);
|
|
511
|
+
expect(found).toBeNull();
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ==========================================
|
|
516
|
+
// WEBHOOK TESTS
|
|
517
|
+
// ==========================================
|
|
518
|
+
|
|
519
|
+
describe('Webhook Operations', () => {
|
|
520
|
+
it('should store webhook event', async () => {
|
|
521
|
+
const webhook = await paymentRepo.storeWebhookEvent({
|
|
522
|
+
provider: 'stripe',
|
|
523
|
+
type: 'payment_intent.succeeded',
|
|
524
|
+
data: {
|
|
525
|
+
id: 'pi_test_123',
|
|
526
|
+
amount: 9999,
|
|
527
|
+
currency: 'usd',
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(webhook).toBeDefined();
|
|
532
|
+
expect(webhook.id).toBeDefined();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should mark webhook as processed', async () => {
|
|
536
|
+
const webhook = await paymentRepo.storeWebhookEvent({
|
|
537
|
+
provider: 'stripe',
|
|
538
|
+
type: 'payment_intent.succeeded',
|
|
539
|
+
data: { id: 'pi_test_123' },
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await paymentRepo.markWebhookProcessed(webhook.id);
|
|
543
|
+
|
|
544
|
+
// Verify it's marked as processed
|
|
545
|
+
const processed = await prisma.paymentWebhook.findUnique({
|
|
546
|
+
where: { id: webhook.id },
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
expect(processed?.processed).toBe(true);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('should mark webhook as processed with error', async () => {
|
|
553
|
+
const webhook = await paymentRepo.storeWebhookEvent({
|
|
554
|
+
provider: 'stripe',
|
|
555
|
+
type: 'payment_intent.failed',
|
|
556
|
+
data: { id: 'pi_test_456' },
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await paymentRepo.markWebhookProcessed(webhook.id, 'Payment not found');
|
|
560
|
+
|
|
561
|
+
const processed = await prisma.paymentWebhook.findUnique({
|
|
562
|
+
where: { id: webhook.id },
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
expect(processed?.processed).toBe(true);
|
|
566
|
+
expect(processed?.error).toBe('Payment not found');
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// ==========================================
|
|
571
|
+
// ENUM MAPPING TESTS
|
|
572
|
+
// ==========================================
|
|
573
|
+
|
|
574
|
+
describe('Enum Mapping', () => {
|
|
575
|
+
it('should correctly map all payment providers', async () => {
|
|
576
|
+
const providers: Array<'stripe' | 'paypal' | 'mobile_money' | 'manual'> = [
|
|
577
|
+
'stripe',
|
|
578
|
+
'paypal',
|
|
579
|
+
'mobile_money',
|
|
580
|
+
'manual',
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
for (const provider of providers) {
|
|
584
|
+
const payment = await paymentRepo.createPayment({
|
|
585
|
+
userId: testUserId,
|
|
586
|
+
provider,
|
|
587
|
+
method: 'card',
|
|
588
|
+
amount: 10,
|
|
589
|
+
currency: 'USD',
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
expect(payment.provider).toBe(provider);
|
|
593
|
+
|
|
594
|
+
const found = await paymentRepo.findPaymentById(payment.id);
|
|
595
|
+
expect(found?.provider).toBe(provider);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should correctly map all payment statuses', async () => {
|
|
600
|
+
const payment = await paymentRepo.createPayment({
|
|
601
|
+
userId: testUserId,
|
|
602
|
+
provider: 'stripe',
|
|
603
|
+
method: 'card',
|
|
604
|
+
amount: 10,
|
|
605
|
+
currency: 'USD',
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const statuses: Array<
|
|
609
|
+
'pending' | 'processing' | 'completed' | 'failed' | 'refunded' | 'cancelled'
|
|
610
|
+
> = ['pending', 'processing', 'completed', 'failed', 'refunded', 'cancelled'];
|
|
611
|
+
|
|
612
|
+
for (const status of statuses) {
|
|
613
|
+
const updated = await paymentRepo.updatePaymentStatus(payment.id, status);
|
|
614
|
+
expect(updated?.status).toBe(status);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should correctly map all plan intervals', async () => {
|
|
619
|
+
const intervals: Array<'day' | 'week' | 'month' | 'year'> = ['day', 'week', 'month', 'year'];
|
|
620
|
+
|
|
621
|
+
for (const interval of intervals) {
|
|
622
|
+
const plan = await paymentRepo.createPlan({
|
|
623
|
+
name: `${interval} plan`,
|
|
624
|
+
amount: 9.99,
|
|
625
|
+
currency: 'USD',
|
|
626
|
+
interval,
|
|
627
|
+
intervalCount: 1,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
expect(plan.interval).toBe(interval);
|
|
631
|
+
|
|
632
|
+
const found = await paymentRepo.findPlanById(plan.id);
|
|
633
|
+
expect(found?.interval).toBe(interval);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
});
|