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,190 @@
|
|
|
1
|
+
import { logger } from '../../../core/logger.js';
|
|
2
|
+
import type { PayPalConfig, PayPalOrderData, PaymentIntent, Payment } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PayPal Payment Provider
|
|
6
|
+
* Uses PayPal REST API v2
|
|
7
|
+
*/
|
|
8
|
+
export class PayPalProvider {
|
|
9
|
+
private config: PayPalConfig;
|
|
10
|
+
private accessToken: string | null = null;
|
|
11
|
+
private tokenExpiry: number = 0;
|
|
12
|
+
|
|
13
|
+
constructor(config: PayPalConfig) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
logger.info({ sandbox: config.sandbox }, 'PayPal provider initialized');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private get baseUrl(): string {
|
|
19
|
+
return this.config.sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async getAccessToken(): Promise<string> {
|
|
23
|
+
if (this.accessToken && Date.now() < this.tokenExpiry) {
|
|
24
|
+
return this.accessToken;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const auth = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString(
|
|
28
|
+
'base64'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const response = await fetch(`${this.baseUrl}/v1/oauth2/token`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Basic ${auth}`,
|
|
35
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
36
|
+
},
|
|
37
|
+
body: 'grant_type=client_credentials',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error('Failed to get PayPal access token');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = (await response.json()) as { access_token: string; expires_in: number };
|
|
45
|
+
this.accessToken = data.access_token;
|
|
46
|
+
this.tokenExpiry = Date.now() + (data.expires_in - 60) * 1000;
|
|
47
|
+
|
|
48
|
+
return this.accessToken;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async createOrder(data: PayPalOrderData): Promise<PaymentIntent> {
|
|
52
|
+
const accessToken = await this.getAccessToken();
|
|
53
|
+
|
|
54
|
+
const response = await fetch(`${this.baseUrl}/v2/checkout/orders`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${accessToken}`,
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
intent: 'CAPTURE',
|
|
62
|
+
purchase_units: [
|
|
63
|
+
{
|
|
64
|
+
amount: {
|
|
65
|
+
currency_code: data.currency.toUpperCase(),
|
|
66
|
+
value: data.amount.toFixed(2),
|
|
67
|
+
},
|
|
68
|
+
description: data.description,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
application_context: {
|
|
72
|
+
return_url: data.returnUrl,
|
|
73
|
+
cancel_url: data.cancelUrl,
|
|
74
|
+
brand_name: 'Servcraft',
|
|
75
|
+
landing_page: 'LOGIN',
|
|
76
|
+
user_action: 'PAY_NOW',
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const error = await response.text();
|
|
83
|
+
logger.error({ error }, 'PayPal order creation failed');
|
|
84
|
+
throw new Error('Failed to create PayPal order');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const order = (await response.json()) as {
|
|
88
|
+
id: string;
|
|
89
|
+
status: string;
|
|
90
|
+
links: Array<{ rel: string; href: string }>;
|
|
91
|
+
};
|
|
92
|
+
const approveLink = order.links.find((l) => l.rel === 'approve');
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: order.id,
|
|
96
|
+
paymentUrl: approveLink?.href,
|
|
97
|
+
status: this.mapPayPalStatus(order.status),
|
|
98
|
+
provider: 'paypal',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async captureOrder(orderId: string): Promise<PaymentIntent> {
|
|
103
|
+
const accessToken = await this.getAccessToken();
|
|
104
|
+
|
|
105
|
+
const response = await fetch(`${this.baseUrl}/v2/checkout/orders/${orderId}/capture`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${accessToken}`,
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error('Failed to capture PayPal order');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = (await response.json()) as { id: string; status: string };
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: result.id,
|
|
121
|
+
status: this.mapPayPalStatus(result.status),
|
|
122
|
+
provider: 'paypal',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async refundPayment(captureId: string, amount?: number, currency?: string): Promise<boolean> {
|
|
127
|
+
const accessToken = await this.getAccessToken();
|
|
128
|
+
|
|
129
|
+
const body: Record<string, unknown> = {};
|
|
130
|
+
if (amount && currency) {
|
|
131
|
+
body.amount = {
|
|
132
|
+
value: amount.toFixed(2),
|
|
133
|
+
currency_code: currency.toUpperCase(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const response = await fetch(`${this.baseUrl}/v2/payments/captures/${captureId}/refund`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Bearer ${accessToken}`,
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify(body),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return response.ok;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getOrderDetails(orderId: string): Promise<Record<string, unknown>> {
|
|
150
|
+
const accessToken = await this.getAccessToken();
|
|
151
|
+
|
|
152
|
+
const response = await fetch(`${this.baseUrl}/v2/checkout/orders/${orderId}`, {
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${accessToken}`,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error('Failed to get PayPal order details');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return response.json() as Promise<Record<string, unknown>>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
verifyWebhookSignature(
|
|
166
|
+
_headers: Record<string, string>,
|
|
167
|
+
_body: string,
|
|
168
|
+
_webhookId: string
|
|
169
|
+
): Promise<boolean> {
|
|
170
|
+
// PayPal webhook verification requires calling their API
|
|
171
|
+
// This is a simplified version
|
|
172
|
+
return Promise.resolve(true);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private mapPayPalStatus(status: string): Payment['status'] {
|
|
176
|
+
const statusMap: Record<string, Payment['status']> = {
|
|
177
|
+
CREATED: 'pending',
|
|
178
|
+
SAVED: 'pending',
|
|
179
|
+
APPROVED: 'processing',
|
|
180
|
+
VOIDED: 'cancelled',
|
|
181
|
+
COMPLETED: 'completed',
|
|
182
|
+
PAYER_ACTION_REQUIRED: 'pending',
|
|
183
|
+
};
|
|
184
|
+
return statusMap[status] || 'pending';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createPayPalProvider(config: PayPalConfig): PayPalProvider {
|
|
189
|
+
return new PayPalProvider(config);
|
|
190
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { logger } from '../../../core/logger.js';
|
|
2
|
+
import type { StripeConfig, StripePaymentData, PaymentIntent, Payment } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stripe Payment Provider
|
|
6
|
+
* Note: Requires 'stripe' package to be installed
|
|
7
|
+
* npm install stripe
|
|
8
|
+
*/
|
|
9
|
+
export class StripeProvider {
|
|
10
|
+
private stripe: unknown;
|
|
11
|
+
private config: StripeConfig;
|
|
12
|
+
|
|
13
|
+
constructor(config: StripeConfig) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.initStripe();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private async initStripe(): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
// Dynamic import to avoid requiring stripe if not used
|
|
21
|
+
const Stripe = await import('stripe');
|
|
22
|
+
this.stripe = new Stripe.default(this.config.secretKey, {
|
|
23
|
+
apiVersion: '2025-02-24.acacia',
|
|
24
|
+
});
|
|
25
|
+
logger.info('Stripe provider initialized');
|
|
26
|
+
} catch {
|
|
27
|
+
logger.warn('Stripe package not installed. Run: npm install stripe');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async createPaymentIntent(data: StripePaymentData): Promise<PaymentIntent> {
|
|
32
|
+
if (!this.stripe) {
|
|
33
|
+
throw new Error('Stripe not initialized');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const stripe = this.stripe as {
|
|
37
|
+
paymentIntents: {
|
|
38
|
+
create: (params: Record<string, unknown>) => Promise<{
|
|
39
|
+
id: string;
|
|
40
|
+
client_secret: string;
|
|
41
|
+
status: string;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const paymentIntent = await stripe.paymentIntents.create({
|
|
47
|
+
amount: Math.round(data.amount * 100), // Convert to cents
|
|
48
|
+
currency: data.currency.toLowerCase(),
|
|
49
|
+
customer: data.customerId,
|
|
50
|
+
payment_method: data.paymentMethodId,
|
|
51
|
+
description: data.description,
|
|
52
|
+
metadata: data.metadata,
|
|
53
|
+
automatic_payment_methods: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
id: paymentIntent.id,
|
|
60
|
+
clientSecret: paymentIntent.client_secret,
|
|
61
|
+
status: this.mapStripeStatus(paymentIntent.status),
|
|
62
|
+
provider: 'stripe',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async confirmPayment(paymentIntentId: string): Promise<PaymentIntent> {
|
|
67
|
+
if (!this.stripe) {
|
|
68
|
+
throw new Error('Stripe not initialized');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stripe = this.stripe as {
|
|
72
|
+
paymentIntents: {
|
|
73
|
+
confirm: (id: string) => Promise<{
|
|
74
|
+
id: string;
|
|
75
|
+
client_secret: string;
|
|
76
|
+
status: string;
|
|
77
|
+
}>;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const paymentIntent = await stripe.paymentIntents.confirm(paymentIntentId);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
id: paymentIntent.id,
|
|
85
|
+
clientSecret: paymentIntent.client_secret,
|
|
86
|
+
status: this.mapStripeStatus(paymentIntent.status),
|
|
87
|
+
provider: 'stripe',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async refundPayment(paymentIntentId: string, amount?: number): Promise<boolean> {
|
|
92
|
+
if (!this.stripe) {
|
|
93
|
+
throw new Error('Stripe not initialized');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const stripe = this.stripe as {
|
|
97
|
+
refunds: {
|
|
98
|
+
create: (params: Record<string, unknown>) => Promise<{ id: string }>;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const refund = await stripe.refunds.create({
|
|
103
|
+
payment_intent: paymentIntentId,
|
|
104
|
+
...(amount ? { amount: Math.round(amount * 100) } : {}),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return !!refund.id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async createCustomer(email: string, name?: string): Promise<string> {
|
|
111
|
+
if (!this.stripe) {
|
|
112
|
+
throw new Error('Stripe not initialized');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const stripe = this.stripe as {
|
|
116
|
+
customers: {
|
|
117
|
+
create: (params: Record<string, unknown>) => Promise<{ id: string }>;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const customer = await stripe.customers.create({
|
|
122
|
+
email,
|
|
123
|
+
name,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return customer.id;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async createSubscription(
|
|
130
|
+
customerId: string,
|
|
131
|
+
priceId: string,
|
|
132
|
+
trialDays?: number
|
|
133
|
+
): Promise<{ subscriptionId: string; clientSecret?: string }> {
|
|
134
|
+
if (!this.stripe) {
|
|
135
|
+
throw new Error('Stripe not initialized');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const stripe = this.stripe as {
|
|
139
|
+
subscriptions: {
|
|
140
|
+
create: (params: Record<string, unknown>) => Promise<{
|
|
141
|
+
id: string;
|
|
142
|
+
latest_invoice: { payment_intent: { client_secret: string } };
|
|
143
|
+
}>;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const subscription = await stripe.subscriptions.create({
|
|
148
|
+
customer: customerId,
|
|
149
|
+
items: [{ price: priceId }],
|
|
150
|
+
payment_behavior: 'default_incomplete',
|
|
151
|
+
expand: ['latest_invoice.payment_intent'],
|
|
152
|
+
...(trialDays ? { trial_period_days: trialDays } : {}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
subscriptionId: subscription.id,
|
|
157
|
+
clientSecret: subscription.latest_invoice?.payment_intent?.client_secret,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async cancelSubscription(subscriptionId: string): Promise<boolean> {
|
|
162
|
+
if (!this.stripe) {
|
|
163
|
+
throw new Error('Stripe not initialized');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const stripe = this.stripe as {
|
|
167
|
+
subscriptions: {
|
|
168
|
+
cancel: (id: string) => Promise<{ id: string }>;
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const result = await stripe.subscriptions.cancel(subscriptionId);
|
|
173
|
+
return !!result.id;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
verifyWebhookSignature(payload: string, signature: string): Record<string, unknown> | null {
|
|
177
|
+
if (!this.stripe || !this.config.webhookSecret) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const stripe = this.stripe as {
|
|
183
|
+
webhooks: {
|
|
184
|
+
constructEvent: (
|
|
185
|
+
payload: string,
|
|
186
|
+
signature: string,
|
|
187
|
+
secret: string
|
|
188
|
+
) => Record<string, unknown>;
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return stripe.webhooks.constructEvent(payload, signature, this.config.webhookSecret);
|
|
193
|
+
} catch {
|
|
194
|
+
logger.error('Stripe webhook signature verification failed');
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private mapStripeStatus(status: string): Payment['status'] {
|
|
200
|
+
const statusMap: Record<string, Payment['status']> = {
|
|
201
|
+
requires_payment_method: 'pending',
|
|
202
|
+
requires_confirmation: 'pending',
|
|
203
|
+
requires_action: 'processing',
|
|
204
|
+
processing: 'processing',
|
|
205
|
+
requires_capture: 'processing',
|
|
206
|
+
canceled: 'cancelled',
|
|
207
|
+
succeeded: 'completed',
|
|
208
|
+
};
|
|
209
|
+
return statusMap[status] || 'pending';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function createStripeProvider(config: StripeConfig): StripeProvider {
|
|
214
|
+
return new StripeProvider(config);
|
|
215
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export type PaymentProvider = 'stripe' | 'paypal' | 'mobile_money' | 'manual';
|
|
2
|
+
export type PaymentStatus =
|
|
3
|
+
| 'pending'
|
|
4
|
+
| 'processing'
|
|
5
|
+
| 'completed'
|
|
6
|
+
| 'failed'
|
|
7
|
+
| 'refunded'
|
|
8
|
+
| 'cancelled';
|
|
9
|
+
export type PaymentMethod =
|
|
10
|
+
| 'card'
|
|
11
|
+
| 'bank_transfer'
|
|
12
|
+
| 'mobile_money'
|
|
13
|
+
| 'paypal'
|
|
14
|
+
| 'crypto'
|
|
15
|
+
| 'cash';
|
|
16
|
+
|
|
17
|
+
export interface Payment {
|
|
18
|
+
id: string;
|
|
19
|
+
userId: string;
|
|
20
|
+
provider: PaymentProvider;
|
|
21
|
+
method: PaymentMethod;
|
|
22
|
+
status: PaymentStatus;
|
|
23
|
+
amount: number;
|
|
24
|
+
currency: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
providerPaymentId?: string;
|
|
28
|
+
providerCustomerId?: string;
|
|
29
|
+
refundedAmount?: number;
|
|
30
|
+
failureReason?: string;
|
|
31
|
+
paidAt?: Date;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
updatedAt: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CreatePaymentData {
|
|
37
|
+
userId: string;
|
|
38
|
+
amount: number;
|
|
39
|
+
currency: string;
|
|
40
|
+
provider: PaymentProvider;
|
|
41
|
+
method?: PaymentMethod;
|
|
42
|
+
description?: string;
|
|
43
|
+
metadata?: Record<string, unknown>;
|
|
44
|
+
returnUrl?: string;
|
|
45
|
+
cancelUrl?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PaymentIntent {
|
|
49
|
+
id: string;
|
|
50
|
+
clientSecret?: string;
|
|
51
|
+
paymentUrl?: string;
|
|
52
|
+
status: PaymentStatus;
|
|
53
|
+
provider: PaymentProvider;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Stripe specific
|
|
57
|
+
export interface StripeConfig {
|
|
58
|
+
secretKey: string;
|
|
59
|
+
publishableKey: string;
|
|
60
|
+
webhookSecret?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StripePaymentData {
|
|
64
|
+
amount: number;
|
|
65
|
+
currency: string;
|
|
66
|
+
customerId?: string;
|
|
67
|
+
paymentMethodId?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
metadata?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// PayPal specific
|
|
73
|
+
export interface PayPalConfig {
|
|
74
|
+
clientId: string;
|
|
75
|
+
clientSecret: string;
|
|
76
|
+
sandbox: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PayPalOrderData {
|
|
80
|
+
amount: number;
|
|
81
|
+
currency: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
returnUrl: string;
|
|
84
|
+
cancelUrl: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Mobile Money (MTN, Orange, Wave, etc.)
|
|
88
|
+
export interface MobileMoneyConfig {
|
|
89
|
+
provider: 'mtn' | 'orange' | 'wave' | 'mpesa';
|
|
90
|
+
apiKey: string;
|
|
91
|
+
apiSecret: string;
|
|
92
|
+
environment: 'sandbox' | 'production';
|
|
93
|
+
callbackUrl: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface MobileMoneyPaymentData {
|
|
97
|
+
amount: number;
|
|
98
|
+
currency: string;
|
|
99
|
+
phoneNumber: string;
|
|
100
|
+
description?: string;
|
|
101
|
+
reference?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Subscription
|
|
105
|
+
export interface Subscription {
|
|
106
|
+
id: string;
|
|
107
|
+
userId: string;
|
|
108
|
+
planId: string;
|
|
109
|
+
provider: PaymentProvider;
|
|
110
|
+
providerSubscriptionId?: string;
|
|
111
|
+
status: 'active' | 'cancelled' | 'past_due' | 'trialing' | 'paused';
|
|
112
|
+
currentPeriodStart: Date;
|
|
113
|
+
currentPeriodEnd: Date;
|
|
114
|
+
cancelAtPeriodEnd: boolean;
|
|
115
|
+
createdAt: Date;
|
|
116
|
+
updatedAt: Date;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface Plan {
|
|
120
|
+
id: string;
|
|
121
|
+
name: string;
|
|
122
|
+
description?: string;
|
|
123
|
+
amount: number;
|
|
124
|
+
currency: string;
|
|
125
|
+
interval: 'day' | 'week' | 'month' | 'year';
|
|
126
|
+
intervalCount: number;
|
|
127
|
+
trialDays?: number;
|
|
128
|
+
features?: string[];
|
|
129
|
+
metadata?: Record<string, unknown>;
|
|
130
|
+
active: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Webhook events
|
|
134
|
+
export interface PaymentWebhookEvent {
|
|
135
|
+
id: string;
|
|
136
|
+
type: string;
|
|
137
|
+
provider: PaymentProvider;
|
|
138
|
+
data: Record<string, unknown>;
|
|
139
|
+
createdAt: Date;
|
|
140
|
+
}
|