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,804 @@
|
|
|
1
|
+
# Payment Module Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Payment module manages payment processing, subscriptions, and billing plans using Prisma ORM with PostgreSQL/MySQL/SQLite support. It provides a complete payment infrastructure with support for multiple payment providers (Stripe, PayPal, Mobile Money).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ✅ Prisma ORM integration (PostgreSQL, MySQL, SQLite)
|
|
10
|
+
- ✅ Multi-provider support (Stripe, PayPal, Mobile Money, Manual)
|
|
11
|
+
- ✅ Payment lifecycle management (pending → completed/failed/refunded)
|
|
12
|
+
- ✅ Subscription billing with plans
|
|
13
|
+
- ✅ Webhook event handling and storage
|
|
14
|
+
- ✅ Pagination and filtering
|
|
15
|
+
- ✅ Financial data persistence
|
|
16
|
+
- ✅ Enum mapping (Prisma ↔ Application types)
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
### Components
|
|
21
|
+
|
|
22
|
+
1. **PaymentRepository** (`payment.repository.ts`)
|
|
23
|
+
- Data access layer using Prisma
|
|
24
|
+
- **Migrated from Map<> to Prisma** ✅ (Completed 2025-12-19)
|
|
25
|
+
- Handles payments, subscriptions, plans, and webhooks
|
|
26
|
+
|
|
27
|
+
2. **PaymentService** (`payment.service.ts`)
|
|
28
|
+
- Business logic layer
|
|
29
|
+
- Integrates with provider-specific implementations
|
|
30
|
+
|
|
31
|
+
3. **Provider Implementations**
|
|
32
|
+
- Stripe Provider (`providers/stripe.provider.ts`)
|
|
33
|
+
- PayPal Provider (`providers/paypal.provider.ts`)
|
|
34
|
+
- Mobile Money Provider (`providers/mobile-money.provider.ts`)
|
|
35
|
+
|
|
36
|
+
## Database Schema
|
|
37
|
+
|
|
38
|
+
### Payment Model
|
|
39
|
+
|
|
40
|
+
```prisma
|
|
41
|
+
model Payment {
|
|
42
|
+
id String @id @default(uuid())
|
|
43
|
+
userId String
|
|
44
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
45
|
+
provider PaymentProvider @default(MANUAL)
|
|
46
|
+
method PaymentMethod @default(CARD)
|
|
47
|
+
status PaymentStatus @default(PENDING)
|
|
48
|
+
amount Float
|
|
49
|
+
currency String @default("USD")
|
|
50
|
+
description String?
|
|
51
|
+
metadata Json?
|
|
52
|
+
providerPaymentId String? @unique
|
|
53
|
+
providerCustomerId String?
|
|
54
|
+
refundedAmount Float?
|
|
55
|
+
failureReason String?
|
|
56
|
+
paidAt DateTime?
|
|
57
|
+
createdAt DateTime @default(now())
|
|
58
|
+
updatedAt DateTime @updatedAt
|
|
59
|
+
|
|
60
|
+
@@index([userId])
|
|
61
|
+
@@index([provider])
|
|
62
|
+
@@index([status])
|
|
63
|
+
@@index([createdAt])
|
|
64
|
+
@@index([providerPaymentId])
|
|
65
|
+
@@map("payments")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
enum PaymentProvider {
|
|
69
|
+
STRIPE
|
|
70
|
+
PAYPAL
|
|
71
|
+
MOBILE_MONEY
|
|
72
|
+
MANUAL
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
enum PaymentStatus {
|
|
76
|
+
PENDING
|
|
77
|
+
PROCESSING
|
|
78
|
+
COMPLETED
|
|
79
|
+
FAILED
|
|
80
|
+
REFUNDED
|
|
81
|
+
CANCELLED
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
enum PaymentMethod {
|
|
85
|
+
CARD
|
|
86
|
+
BANK_TRANSFER
|
|
87
|
+
MOBILE_MONEY
|
|
88
|
+
PAYPAL
|
|
89
|
+
CRYPTO
|
|
90
|
+
CASH
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Subscription Model
|
|
95
|
+
|
|
96
|
+
```prisma
|
|
97
|
+
model Subscription {
|
|
98
|
+
id String @id @default(uuid())
|
|
99
|
+
userId String
|
|
100
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
101
|
+
planId String
|
|
102
|
+
plan Plan @relation(fields: [planId], references: [id], onDelete: Restrict)
|
|
103
|
+
provider PaymentProvider @default(STRIPE)
|
|
104
|
+
providerSubscriptionId String? @unique
|
|
105
|
+
status SubscriptionStatus @default(ACTIVE)
|
|
106
|
+
currentPeriodStart DateTime
|
|
107
|
+
currentPeriodEnd DateTime
|
|
108
|
+
cancelAtPeriodEnd Boolean @default(false)
|
|
109
|
+
createdAt DateTime @default(now())
|
|
110
|
+
updatedAt DateTime @updatedAt
|
|
111
|
+
|
|
112
|
+
@@index([userId])
|
|
113
|
+
@@index([planId])
|
|
114
|
+
@@index([status])
|
|
115
|
+
@@map("subscriptions")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
enum SubscriptionStatus {
|
|
119
|
+
ACTIVE
|
|
120
|
+
CANCELLED
|
|
121
|
+
PAST_DUE
|
|
122
|
+
TRIALING
|
|
123
|
+
PAUSED
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Plan Model
|
|
128
|
+
|
|
129
|
+
```prisma
|
|
130
|
+
model Plan {
|
|
131
|
+
id String @id @default(uuid())
|
|
132
|
+
name String
|
|
133
|
+
description String?
|
|
134
|
+
amount Float
|
|
135
|
+
currency String @default("USD")
|
|
136
|
+
interval PlanInterval @default(MONTH)
|
|
137
|
+
intervalCount Int @default(1)
|
|
138
|
+
trialDays Int?
|
|
139
|
+
features Json?
|
|
140
|
+
metadata Json?
|
|
141
|
+
active Boolean @default(true)
|
|
142
|
+
createdAt DateTime @default(now())
|
|
143
|
+
updatedAt DateTime @updatedAt
|
|
144
|
+
|
|
145
|
+
subscriptions Subscription[]
|
|
146
|
+
|
|
147
|
+
@@index([active])
|
|
148
|
+
@@map("plans")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
enum PlanInterval {
|
|
152
|
+
DAY
|
|
153
|
+
WEEK
|
|
154
|
+
MONTH
|
|
155
|
+
YEAR
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Webhook Model
|
|
160
|
+
|
|
161
|
+
```prisma
|
|
162
|
+
model PaymentWebhook {
|
|
163
|
+
id String @id @default(uuid())
|
|
164
|
+
provider PaymentProvider
|
|
165
|
+
type String
|
|
166
|
+
data Json
|
|
167
|
+
processed Boolean @default(false)
|
|
168
|
+
error String?
|
|
169
|
+
createdAt DateTime @default(now())
|
|
170
|
+
|
|
171
|
+
@@index([provider])
|
|
172
|
+
@@index([processed])
|
|
173
|
+
@@map("payment_webhooks")
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Repository API
|
|
178
|
+
|
|
179
|
+
### Payment Methods
|
|
180
|
+
|
|
181
|
+
#### `createPayment(data): Promise<Payment>`
|
|
182
|
+
|
|
183
|
+
Create a new payment.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const payment = await paymentRepo.createPayment({
|
|
187
|
+
userId: 'user-id',
|
|
188
|
+
provider: 'stripe',
|
|
189
|
+
method: 'card',
|
|
190
|
+
amount: 99.99,
|
|
191
|
+
currency: 'USD',
|
|
192
|
+
description: 'Premium subscription',
|
|
193
|
+
metadata: { orderId: '12345' },
|
|
194
|
+
providerPaymentId: 'pi_stripe_123',
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### `findPaymentById(id): Promise<Payment | null>`
|
|
199
|
+
|
|
200
|
+
Find payment by ID.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const payment = await paymentRepo.findPaymentById('payment-id');
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `findPaymentByProviderPaymentId(providerPaymentId): Promise<Payment | null>`
|
|
207
|
+
|
|
208
|
+
Find payment by provider's payment ID (e.g., Stripe payment intent ID).
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const payment = await paymentRepo.findPaymentByProviderPaymentId('pi_stripe_123');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### `findUserPayments(userId, params): Promise<PaginatedResult<Payment>>`
|
|
215
|
+
|
|
216
|
+
Find user's payments with pagination.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const result = await paymentRepo.findUserPayments('user-id', {
|
|
220
|
+
page: 1,
|
|
221
|
+
limit: 10,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
console.log(result.data); // Payment[]
|
|
225
|
+
console.log(result.meta); // { page, limit, total, totalPages, hasMore }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### `findPayments(params, filters?): Promise<PaginatedResult<Payment>>`
|
|
229
|
+
|
|
230
|
+
Find payments with filters and pagination.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
const result = await paymentRepo.findPayments(
|
|
234
|
+
{ page: 1, limit: 20 },
|
|
235
|
+
{
|
|
236
|
+
provider: 'stripe',
|
|
237
|
+
status: 'completed',
|
|
238
|
+
startDate: new Date('2024-01-01'),
|
|
239
|
+
endDate: new Date('2024-12-31'),
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Filter options:**
|
|
245
|
+
- `userId`: Filter by user
|
|
246
|
+
- `provider`: Filter by payment provider
|
|
247
|
+
- `status`: Filter by payment status
|
|
248
|
+
- `startDate`: Filter payments after date
|
|
249
|
+
- `endDate`: Filter payments before date
|
|
250
|
+
|
|
251
|
+
#### `updatePaymentStatus(id, status, data?): Promise<Payment | null>`
|
|
252
|
+
|
|
253
|
+
Update payment status.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// Mark as completed
|
|
257
|
+
await paymentRepo.updatePaymentStatus('payment-id', 'completed', {
|
|
258
|
+
paidAt: new Date(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Mark as failed
|
|
262
|
+
await paymentRepo.updatePaymentStatus('payment-id', 'failed', {
|
|
263
|
+
failureReason: 'Insufficient funds',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Mark as refunded
|
|
267
|
+
await paymentRepo.updatePaymentStatus('payment-id', 'refunded', {
|
|
268
|
+
refundedAmount: 99.99,
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### `deletePayment(id): Promise<boolean>`
|
|
273
|
+
|
|
274
|
+
Delete payment by ID.
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const deleted = await paymentRepo.deletePayment('payment-id');
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### `countPayments(filters?): Promise<number>`
|
|
281
|
+
|
|
282
|
+
Count payments with filters.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const total = await paymentRepo.countPayments();
|
|
286
|
+
const completed = await paymentRepo.countPayments({ status: 'completed' });
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Subscription Methods
|
|
290
|
+
|
|
291
|
+
#### `createSubscription(data): Promise<Subscription>`
|
|
292
|
+
|
|
293
|
+
Create a new subscription.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const subscription = await paymentRepo.createSubscription({
|
|
297
|
+
userId: 'user-id',
|
|
298
|
+
planId: 'plan-id',
|
|
299
|
+
provider: 'stripe',
|
|
300
|
+
currentPeriodStart: new Date(),
|
|
301
|
+
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // +30 days
|
|
302
|
+
providerSubscriptionId: 'sub_stripe_123',
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### `findSubscriptionById(id): Promise<Subscription | null>`
|
|
307
|
+
|
|
308
|
+
Find subscription by ID (includes plan details).
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const subscription = await paymentRepo.findSubscriptionById('subscription-id');
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### `findUserSubscriptions(userId): Promise<Subscription[]>`
|
|
315
|
+
|
|
316
|
+
Find user's subscriptions.
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
const subscriptions = await paymentRepo.findUserSubscriptions('user-id');
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### `updateSubscriptionStatus(id, status): Promise<Subscription | null>`
|
|
323
|
+
|
|
324
|
+
Update subscription status.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
await paymentRepo.updateSubscriptionStatus('subscription-id', 'past_due');
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### `cancelSubscription(id): Promise<Subscription | null>`
|
|
331
|
+
|
|
332
|
+
Cancel subscription at period end.
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
await paymentRepo.cancelSubscription('subscription-id');
|
|
336
|
+
// Sets cancelAtPeriodEnd = true
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### `deleteSubscription(id): Promise<boolean>`
|
|
340
|
+
|
|
341
|
+
Delete subscription immediately.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
await paymentRepo.deleteSubscription('subscription-id');
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Plan Methods
|
|
348
|
+
|
|
349
|
+
#### `createPlan(data): Promise<Plan>`
|
|
350
|
+
|
|
351
|
+
Create a new plan.
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
const plan = await paymentRepo.createPlan({
|
|
355
|
+
name: 'Premium',
|
|
356
|
+
description: 'Premium features',
|
|
357
|
+
amount: 29.99,
|
|
358
|
+
currency: 'USD',
|
|
359
|
+
interval: 'month',
|
|
360
|
+
intervalCount: 1,
|
|
361
|
+
trialDays: 7,
|
|
362
|
+
features: ['Feature 1', 'Feature 2', 'Feature 3'],
|
|
363
|
+
metadata: { priority: 'high' },
|
|
364
|
+
active: true,
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### `findPlanById(id): Promise<Plan | null>`
|
|
369
|
+
|
|
370
|
+
Find plan by ID.
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
const plan = await paymentRepo.findPlanById('plan-id');
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
#### `findActivePlans(): Promise<Plan[]>`
|
|
377
|
+
|
|
378
|
+
Find all active plans (sorted by amount).
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const plans = await paymentRepo.findActivePlans();
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### `updatePlan(id, data): Promise<Plan | null>`
|
|
385
|
+
|
|
386
|
+
Update plan.
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
await paymentRepo.updatePlan('plan-id', {
|
|
390
|
+
name: 'Premium Plus',
|
|
391
|
+
amount: 39.99,
|
|
392
|
+
active: true,
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
#### `deletePlan(id): Promise<boolean>`
|
|
397
|
+
|
|
398
|
+
Delete plan.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
await paymentRepo.deletePlan('plan-id');
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Webhook Methods
|
|
405
|
+
|
|
406
|
+
#### `storeWebhookEvent(data): Promise<{ id: string }>`
|
|
407
|
+
|
|
408
|
+
Store webhook event for processing.
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
const webhook = await paymentRepo.storeWebhookEvent({
|
|
412
|
+
provider: 'stripe',
|
|
413
|
+
type: 'payment_intent.succeeded',
|
|
414
|
+
data: {
|
|
415
|
+
id: 'pi_123',
|
|
416
|
+
amount: 9999,
|
|
417
|
+
currency: 'usd',
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
#### `markWebhookProcessed(id, error?): Promise<void>`
|
|
423
|
+
|
|
424
|
+
Mark webhook as processed.
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
// Success
|
|
428
|
+
await paymentRepo.markWebhookProcessed(webhook.id);
|
|
429
|
+
|
|
430
|
+
// With error
|
|
431
|
+
await paymentRepo.markWebhookProcessed(webhook.id, 'Payment not found');
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Type Mapping
|
|
435
|
+
|
|
436
|
+
The repository handles enum conversions between Prisma (UPPERCASE) and application types (lowercase):
|
|
437
|
+
|
|
438
|
+
### Payment Providers
|
|
439
|
+
|
|
440
|
+
| Application Type | Prisma Enum |
|
|
441
|
+
|------------------|-------------|
|
|
442
|
+
| `stripe` | `PaymentProvider.STRIPE` |
|
|
443
|
+
| `paypal` | `PaymentProvider.PAYPAL` |
|
|
444
|
+
| `mobile_money` | `PaymentProvider.MOBILE_MONEY` |
|
|
445
|
+
| `manual` | `PaymentProvider.MANUAL` |
|
|
446
|
+
|
|
447
|
+
### Payment Status
|
|
448
|
+
|
|
449
|
+
| Application Type | Prisma Enum |
|
|
450
|
+
|------------------|-------------|
|
|
451
|
+
| `pending` | `PaymentStatus.PENDING` |
|
|
452
|
+
| `processing` | `PaymentStatus.PROCESSING` |
|
|
453
|
+
| `completed` | `PaymentStatus.COMPLETED` |
|
|
454
|
+
| `failed` | `PaymentStatus.FAILED` |
|
|
455
|
+
| `refunded` | `PaymentStatus.REFUNDED` |
|
|
456
|
+
| `cancelled` | `PaymentStatus.CANCELLED` |
|
|
457
|
+
|
|
458
|
+
### Payment Methods
|
|
459
|
+
|
|
460
|
+
| Application Type | Prisma Enum |
|
|
461
|
+
|------------------|-------------|
|
|
462
|
+
| `card` | `PaymentMethod.CARD` |
|
|
463
|
+
| `bank_transfer` | `PaymentMethod.BANK_TRANSFER` |
|
|
464
|
+
| `mobile_money` | `PaymentMethod.MOBILE_MONEY` |
|
|
465
|
+
| `paypal` | `PaymentMethod.PAYPAL` |
|
|
466
|
+
| `crypto` | `PaymentMethod.CRYPTO` |
|
|
467
|
+
| `cash` | `PaymentMethod.CASH` |
|
|
468
|
+
|
|
469
|
+
### Subscription Status
|
|
470
|
+
|
|
471
|
+
| Application Type | Prisma Enum |
|
|
472
|
+
|------------------|-------------|
|
|
473
|
+
| `active` | `SubscriptionStatus.ACTIVE` |
|
|
474
|
+
| `cancelled` | `SubscriptionStatus.CANCELLED` |
|
|
475
|
+
| `past_due` | `SubscriptionStatus.PAST_DUE` |
|
|
476
|
+
| `trialing` | `SubscriptionStatus.TRIALING` |
|
|
477
|
+
| `paused` | `SubscriptionStatus.PAUSED` |
|
|
478
|
+
|
|
479
|
+
### Plan Intervals
|
|
480
|
+
|
|
481
|
+
| Application Type | Prisma Enum |
|
|
482
|
+
|------------------|-------------|
|
|
483
|
+
| `day` | `PlanInterval.DAY` |
|
|
484
|
+
| `week` | `PlanInterval.WEEK` |
|
|
485
|
+
| `month` | `PlanInterval.MONTH` |
|
|
486
|
+
| `year` | `PlanInterval.YEAR` |
|
|
487
|
+
|
|
488
|
+
## Usage Examples
|
|
489
|
+
|
|
490
|
+
### Complete Payment Flow
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import { PaymentRepository } from './modules/payment/payment.repository.js';
|
|
494
|
+
|
|
495
|
+
const paymentRepo = new PaymentRepository();
|
|
496
|
+
|
|
497
|
+
// 1. Create payment
|
|
498
|
+
const payment = await paymentRepo.createPayment({
|
|
499
|
+
userId: 'user-123',
|
|
500
|
+
provider: 'stripe',
|
|
501
|
+
method: 'card',
|
|
502
|
+
amount: 99.99,
|
|
503
|
+
currency: 'USD',
|
|
504
|
+
description: 'Premium subscription - Annual',
|
|
505
|
+
providerPaymentId: 'pi_stripe_abc123',
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// 2. Process payment (handled by provider webhook)
|
|
509
|
+
// When Stripe sends webhook: payment_intent.succeeded
|
|
510
|
+
|
|
511
|
+
// 3. Update payment status
|
|
512
|
+
await paymentRepo.updatePaymentStatus(payment.id, 'completed', {
|
|
513
|
+
paidAt: new Date(),
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// 4. If refund needed
|
|
517
|
+
await paymentRepo.updatePaymentStatus(payment.id, 'refunded', {
|
|
518
|
+
refundedAmount: 99.99,
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Subscription Management
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
// 1. Create plan
|
|
526
|
+
const plan = await paymentRepo.createPlan({
|
|
527
|
+
name: 'Pro Plan',
|
|
528
|
+
amount: 19.99,
|
|
529
|
+
currency: 'USD',
|
|
530
|
+
interval: 'month',
|
|
531
|
+
intervalCount: 1,
|
|
532
|
+
trialDays: 14,
|
|
533
|
+
features: ['Unlimited projects', '24/7 support', 'Advanced analytics'],
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// 2. Subscribe user
|
|
537
|
+
const currentPeriodStart = new Date();
|
|
538
|
+
const currentPeriodEnd = new Date();
|
|
539
|
+
currentPeriodEnd.setMonth(currentPeriodEnd.getMonth() + 1);
|
|
540
|
+
|
|
541
|
+
const subscription = await paymentRepo.createSubscription({
|
|
542
|
+
userId: 'user-123',
|
|
543
|
+
planId: plan.id,
|
|
544
|
+
provider: 'stripe',
|
|
545
|
+
currentPeriodStart,
|
|
546
|
+
currentPeriodEnd,
|
|
547
|
+
providerSubscriptionId: 'sub_stripe_xyz789',
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// 3. User wants to cancel
|
|
551
|
+
await paymentRepo.cancelSubscription(subscription.id);
|
|
552
|
+
// Will cancel at period end
|
|
553
|
+
|
|
554
|
+
// 4. Check user subscriptions
|
|
555
|
+
const userSubs = await paymentRepo.findUserSubscriptions('user-123');
|
|
556
|
+
console.log(`Active subscriptions: ${userSubs.filter(s => s.status === 'active').length}`);
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Admin Dashboard - Payment Analytics
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// Get completed payments for last month
|
|
563
|
+
const lastMonth = new Date();
|
|
564
|
+
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
|
565
|
+
|
|
566
|
+
const result = await paymentRepo.findPayments(
|
|
567
|
+
{ page: 1, limit: 100 },
|
|
568
|
+
{
|
|
569
|
+
status: 'completed',
|
|
570
|
+
startDate: lastMonth,
|
|
571
|
+
endDate: new Date(),
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
// Calculate revenue
|
|
576
|
+
const totalRevenue = result.data.reduce((sum, payment) => sum + payment.amount, 0);
|
|
577
|
+
console.log(`Revenue: $${totalRevenue.toFixed(2)}`);
|
|
578
|
+
|
|
579
|
+
// Count by provider
|
|
580
|
+
const stripeCount = await paymentRepo.countPayments({
|
|
581
|
+
provider: 'stripe',
|
|
582
|
+
status: 'completed',
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const paypalCount = await paymentRepo.countPayments({
|
|
586
|
+
provider: 'paypal',
|
|
587
|
+
status: 'completed',
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
console.log(`Stripe: ${stripeCount}, PayPal: ${paypalCount}`);
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Webhook Handling
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// Store webhook for processing
|
|
597
|
+
const webhook = await paymentRepo.storeWebhookEvent({
|
|
598
|
+
provider: 'stripe',
|
|
599
|
+
type: 'payment_intent.succeeded',
|
|
600
|
+
data: {
|
|
601
|
+
id: 'pi_stripe_123',
|
|
602
|
+
amount: 9999,
|
|
603
|
+
currency: 'usd',
|
|
604
|
+
status: 'succeeded',
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
// Process webhook
|
|
610
|
+
const payment = await paymentRepo.findPaymentByProviderPaymentId('pi_stripe_123');
|
|
611
|
+
if (payment) {
|
|
612
|
+
await paymentRepo.updatePaymentStatus(payment.id, 'completed', {
|
|
613
|
+
paidAt: new Date(),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Mark as processed
|
|
618
|
+
await paymentRepo.markWebhookProcessed(webhook.id);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
// Mark as failed
|
|
621
|
+
await paymentRepo.markWebhookProcessed(webhook.id, error.message);
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Migration from In-Memory
|
|
626
|
+
|
|
627
|
+
**Previous implementation** (v0.1.0):
|
|
628
|
+
```typescript
|
|
629
|
+
// ❌ OLD: In-memory storage
|
|
630
|
+
const payments = new Map<string, Payment>();
|
|
631
|
+
const subscriptions = new Map<string, Subscription>();
|
|
632
|
+
const plans = new Map<string, Plan>();
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Current implementation** (v0.2.0):
|
|
636
|
+
```typescript
|
|
637
|
+
// ✅ NEW: Prisma ORM
|
|
638
|
+
await prisma.payment.create({ data: { ... } });
|
|
639
|
+
await prisma.subscription.create({ data: { ... } });
|
|
640
|
+
await prisma.plan.create({ data: { ... } });
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Migration Steps
|
|
644
|
+
|
|
645
|
+
1. **Backup existing data** (if migrating from production):
|
|
646
|
+
```bash
|
|
647
|
+
# Export in-memory data to JSON before migration
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
2. **Run migrations**:
|
|
651
|
+
```bash
|
|
652
|
+
npm run db:migrate
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
3. **Verify schema**:
|
|
656
|
+
```bash
|
|
657
|
+
npm run db:generate
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
4. **Import data** (if needed):
|
|
661
|
+
```typescript
|
|
662
|
+
// Bulk import payments, subscriptions, and plans
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## Performance Considerations
|
|
666
|
+
|
|
667
|
+
### Indexes
|
|
668
|
+
|
|
669
|
+
The Payment tables have indexes on:
|
|
670
|
+
- `userId` - Fast user payment lookups
|
|
671
|
+
- `provider` - Filter by payment provider
|
|
672
|
+
- `status` - Filter by payment status
|
|
673
|
+
- `createdAt` - Date range queries
|
|
674
|
+
- `providerPaymentId` - Provider webhook lookups (unique)
|
|
675
|
+
|
|
676
|
+
### Query Optimization
|
|
677
|
+
|
|
678
|
+
- Use `findPaymentByProviderPaymentId()` for webhook processing (indexed)
|
|
679
|
+
- Leverage pagination for large result sets
|
|
680
|
+
- Filter by date ranges for analytics
|
|
681
|
+
- Use `countPayments()` for statistics instead of loading all data
|
|
682
|
+
|
|
683
|
+
### Financial Data Best Practices
|
|
684
|
+
|
|
685
|
+
- **Use Decimal/Float carefully**: Store amounts in smallest unit (cents) or use Decimal type
|
|
686
|
+
- **Idempotency**: Check `providerPaymentId` before creating duplicate payments
|
|
687
|
+
- **Audit trail**: Never delete payments, update status instead
|
|
688
|
+
- **Currency precision**: Always store currency code with amount
|
|
689
|
+
- **Refunds**: Track original amount and refunded amount separately
|
|
690
|
+
|
|
691
|
+
## Security Considerations
|
|
692
|
+
|
|
693
|
+
### PCI Compliance
|
|
694
|
+
|
|
695
|
+
- **Never store**: Card numbers, CVV, expiration dates
|
|
696
|
+
- **Store only**: Provider payment IDs and customer IDs
|
|
697
|
+
- **Use**: Provider-hosted payment forms (Stripe Elements, PayPal Checkout)
|
|
698
|
+
|
|
699
|
+
### Webhook Security
|
|
700
|
+
|
|
701
|
+
- **Verify signatures**: Always validate webhook signatures
|
|
702
|
+
- **Idempotency**: Check if webhook already processed
|
|
703
|
+
- **Error handling**: Store failed webhooks for manual review
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
// Example webhook verification (Stripe)
|
|
707
|
+
const event = stripe.webhooks.constructEvent(
|
|
708
|
+
payload,
|
|
709
|
+
signature,
|
|
710
|
+
process.env.STRIPE_WEBHOOK_SECRET
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
// Store and process
|
|
714
|
+
const webhook = await paymentRepo.storeWebhookEvent({
|
|
715
|
+
provider: 'stripe',
|
|
716
|
+
type: event.type,
|
|
717
|
+
data: event.data.object,
|
|
718
|
+
});
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
## Production Checklist
|
|
722
|
+
|
|
723
|
+
- [x] Prisma client generated (`npm run db:generate`)
|
|
724
|
+
- [x] Migrations applied (`npm run db:migrate`)
|
|
725
|
+
- [ ] Webhook endpoints configured with providers
|
|
726
|
+
- [ ] Provider API keys stored securely (environment variables)
|
|
727
|
+
- [ ] Webhook signature verification enabled
|
|
728
|
+
- [ ] Payment failure notifications configured
|
|
729
|
+
- [ ] Refund process documented
|
|
730
|
+
- [ ] Currency conversion handled (if multi-currency)
|
|
731
|
+
- [ ] Tax calculation implemented (if applicable)
|
|
732
|
+
- [ ] Backup and disaster recovery plan
|
|
733
|
+
|
|
734
|
+
## Troubleshooting
|
|
735
|
+
|
|
736
|
+
### \"Payment not found\" after webhook
|
|
737
|
+
|
|
738
|
+
**Problem**: Webhook received but payment doesn't exist in database.
|
|
739
|
+
|
|
740
|
+
**Solution**:
|
|
741
|
+
1. Check `providerPaymentId` matches
|
|
742
|
+
2. Verify webhook ordering (create before update)
|
|
743
|
+
3. Check if payment creation failed
|
|
744
|
+
|
|
745
|
+
### \"Duplicate payment\" error
|
|
746
|
+
|
|
747
|
+
**Problem**: Same payment created twice.
|
|
748
|
+
|
|
749
|
+
**Solution**:
|
|
750
|
+
- Use `providerPaymentId` (unique constraint)
|
|
751
|
+
- Check idempotency keys
|
|
752
|
+
- Verify webhook deduplication
|
|
753
|
+
|
|
754
|
+
### \"Subscription not cancelling\"
|
|
755
|
+
|
|
756
|
+
**Problem**: `cancelAtPeriodEnd` set but still active.
|
|
757
|
+
|
|
758
|
+
**Solution**:
|
|
759
|
+
- This is correct behavior - subscription remains active until period ends
|
|
760
|
+
- Use cron job to check expired subscriptions and update status
|
|
761
|
+
|
|
762
|
+
## Testing
|
|
763
|
+
|
|
764
|
+
Run payment repository tests:
|
|
765
|
+
```bash
|
|
766
|
+
# All payment tests
|
|
767
|
+
npm test tests/integration/payment-prisma.test.ts
|
|
768
|
+
|
|
769
|
+
# With coverage
|
|
770
|
+
npm run test:coverage -- tests/integration/payment-prisma.test.ts
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
**Test coverage:** 100% (all CRUD operations, filtering, pagination, enum mapping, webhooks)
|
|
774
|
+
|
|
775
|
+
## Related Modules
|
|
776
|
+
|
|
777
|
+
- **Auth Module**: User authentication for payment authorization
|
|
778
|
+
- **Notification Module**: Payment confirmations and receipts
|
|
779
|
+
- **Webhook Module**: Generic webhook handling
|
|
780
|
+
- **Audit Module**: Track payment events
|
|
781
|
+
|
|
782
|
+
## API Reference
|
|
783
|
+
|
|
784
|
+
See `src/modules/payment/types.ts` for complete type definitions.
|
|
785
|
+
|
|
786
|
+
## Changelog
|
|
787
|
+
|
|
788
|
+
### v0.2.0 (2025-12-19)
|
|
789
|
+
|
|
790
|
+
**PAYMENT-001 Completed:**
|
|
791
|
+
- ✅ Migrated from `Map<>` to Prisma ORM
|
|
792
|
+
- ✅ Added full test coverage (45+ integration tests)
|
|
793
|
+
- ✅ Implemented enum mapping (Prisma ↔ Application)
|
|
794
|
+
- ✅ Added subscription and plan management
|
|
795
|
+
- ✅ Implemented webhook storage and tracking
|
|
796
|
+
- ✅ Preserved API compatibility
|
|
797
|
+
- ✅ Added this documentation
|
|
798
|
+
|
|
799
|
+
### v0.1.0 (Initial)
|
|
800
|
+
|
|
801
|
+
- In-memory storage with Map<>
|
|
802
|
+
- Basic payment operations
|
|
803
|
+
- No persistence across restarts
|
|
804
|
+
- Limited provider support
|