@soulbatical/tetra-core 0.10.4 → 0.11.0
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/README.md +78 -38
- package/dist/core/createApp.d.ts +1 -1
- package/dist/core/createApp.d.ts.map +1 -1
- package/dist/core/createApp.js +77 -2
- package/dist/core/createApp.js.map +1 -1
- package/dist/core/dualWriteProxy.d.ts +7 -2
- package/dist/core/dualWriteProxy.d.ts.map +1 -1
- package/dist/core/dualWriteProxy.js +16 -5
- package/dist/core/dualWriteProxy.js.map +1 -1
- package/dist/core/routeContext.d.ts +24 -0
- package/dist/core/routeContext.d.ts.map +1 -1
- package/dist/core/routeContext.js +31 -4
- package/dist/core/routeContext.js.map +1 -1
- package/dist/core/systemDb.d.ts +2 -2
- package/dist/core/systemDb.js +2 -2
- package/dist/generators/rls-checker.d.ts +1 -1
- package/dist/generators/rls-checker.js +1 -1
- package/dist/generators/rls-exec-sql.d.ts +1 -1
- package/dist/generators/rls-exec-sql.js +1 -1
- package/dist/generators/rpc/index.d.ts +1 -1
- package/dist/generators/rpc/index.js +1 -1
- package/dist/index.d.ts +3 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -32
- package/dist/index.js.map +1 -1
- package/dist/middleware/securityMiddleware.d.ts +1 -1
- package/dist/middleware/securityMiddleware.d.ts.map +1 -1
- package/dist/middleware/validateBody.d.ts.map +1 -1
- package/dist/middleware/validateBody.js +51 -8
- package/dist/middleware/validateBody.js.map +1 -1
- package/dist/shared/rfc7807ErrorResponse.d.ts +7 -0
- package/dist/shared/rfc7807ErrorResponse.d.ts.map +1 -1
- package/dist/shared/rfc7807ErrorResponse.js +19 -5
- package/dist/shared/rfc7807ErrorResponse.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +16 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +33 -77
- package/dist/affiliate.d.ts +0 -11
- package/dist/affiliate.d.ts.map +0 -1
- package/dist/affiliate.js +0 -10
- package/dist/affiliate.js.map +0 -1
- package/dist/billing.d.ts +0 -8
- package/dist/billing.d.ts.map +0 -1
- package/dist/billing.js +0 -7
- package/dist/billing.js.map +0 -1
- package/dist/email.d.ts +0 -9
- package/dist/email.d.ts.map +0 -1
- package/dist/email.js +0 -8
- package/dist/email.js.map +0 -1
- package/dist/generators/rls-exec-sql.sql +0 -57
- package/dist/generators.d.ts +0 -15
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js +0 -12
- package/dist/generators.js.map +0 -1
- package/dist/mcp.d.ts +0 -8
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js +0 -7
- package/dist/mcp.js.map +0 -1
- package/dist/planner.d.ts +0 -8
- package/dist/planner.d.ts.map +0 -1
- package/dist/planner.js +0 -7
- package/dist/planner.js.map +0 -1
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts +0 -47
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +0 -1
- package/dist/shared/affiliate/AffiliateAttributionService.js +0 -308
- package/dist/shared/affiliate/AffiliateAttributionService.js.map +0 -1
- package/dist/shared/affiliate/AffiliateClickService.d.ts +0 -35
- package/dist/shared/affiliate/AffiliateClickService.d.ts.map +0 -1
- package/dist/shared/affiliate/AffiliateClickService.js +0 -87
- package/dist/shared/affiliate/AffiliateClickService.js.map +0 -1
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +0 -11
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +0 -1
- package/dist/shared/affiliate/affiliateFeatureConfig.js +0 -242
- package/dist/shared/affiliate/affiliateFeatureConfig.js.map +0 -1
- package/dist/shared/affiliate/index.d.ts +0 -11
- package/dist/shared/affiliate/index.d.ts.map +0 -1
- package/dist/shared/affiliate/index.js +0 -13
- package/dist/shared/affiliate/index.js.map +0 -1
- package/dist/shared/affiliate/routes.d.ts +0 -87
- package/dist/shared/affiliate/routes.d.ts.map +0 -1
- package/dist/shared/affiliate/routes.js +0 -404
- package/dist/shared/affiliate/routes.js.map +0 -1
- package/dist/shared/affiliate/types.d.ts +0 -170
- package/dist/shared/affiliate/types.d.ts.map +0 -1
- package/dist/shared/affiliate/types.js +0 -11
- package/dist/shared/affiliate/types.js.map +0 -1
- package/dist/shared/billing/BillingService.d.ts +0 -56
- package/dist/shared/billing/BillingService.d.ts.map +0 -1
- package/dist/shared/billing/BillingService.js +0 -588
- package/dist/shared/billing/BillingService.js.map +0 -1
- package/dist/shared/billing/SeatBillingService.d.ts +0 -106
- package/dist/shared/billing/SeatBillingService.d.ts.map +0 -1
- package/dist/shared/billing/SeatBillingService.js +0 -292
- package/dist/shared/billing/SeatBillingService.js.map +0 -1
- package/dist/shared/billing/index.d.ts +0 -30
- package/dist/shared/billing/index.d.ts.map +0 -1
- package/dist/shared/billing/index.js +0 -27
- package/dist/shared/billing/index.js.map +0 -1
- package/dist/shared/billing/routes.d.ts +0 -45
- package/dist/shared/billing/routes.d.ts.map +0 -1
- package/dist/shared/billing/routes.js +0 -184
- package/dist/shared/billing/routes.js.map +0 -1
- package/dist/shared/billing/seat-pricing.d.ts +0 -53
- package/dist/shared/billing/seat-pricing.d.ts.map +0 -1
- package/dist/shared/billing/seat-pricing.js +0 -81
- package/dist/shared/billing/seat-pricing.js.map +0 -1
- package/dist/shared/billing/types.d.ts +0 -109
- package/dist/shared/billing/types.d.ts.map +0 -1
- package/dist/shared/billing/types.js +0 -8
- package/dist/shared/billing/types.js.map +0 -1
- package/dist/shared/email/EmailService.d.ts +0 -64
- package/dist/shared/email/EmailService.d.ts.map +0 -1
- package/dist/shared/email/EmailService.js +0 -300
- package/dist/shared/email/EmailService.js.map +0 -1
- package/dist/shared/email/adminRoutes.d.ts +0 -30
- package/dist/shared/email/adminRoutes.d.ts.map +0 -1
- package/dist/shared/email/adminRoutes.js +0 -227
- package/dist/shared/email/adminRoutes.js.map +0 -1
- package/dist/shared/email/gmail.d.ts +0 -208
- package/dist/shared/email/gmail.d.ts.map +0 -1
- package/dist/shared/email/gmail.js +0 -626
- package/dist/shared/email/gmail.js.map +0 -1
- package/dist/shared/email/index.d.ts +0 -15
- package/dist/shared/email/index.d.ts.map +0 -1
- package/dist/shared/email/index.js +0 -18
- package/dist/shared/email/index.js.map +0 -1
- package/dist/shared/email/mailgun.d.ts +0 -18
- package/dist/shared/email/mailgun.d.ts.map +0 -1
- package/dist/shared/email/mailgun.js +0 -76
- package/dist/shared/email/mailgun.js.map +0 -1
- package/dist/shared/email/sanitize.d.ts +0 -25
- package/dist/shared/email/sanitize.d.ts.map +0 -1
- package/dist/shared/email/sanitize.js +0 -39
- package/dist/shared/email/sanitize.js.map +0 -1
- package/dist/shared/email/smtp.d.ts +0 -20
- package/dist/shared/email/smtp.d.ts.map +0 -1
- package/dist/shared/email/smtp.js +0 -53
- package/dist/shared/email/smtp.js.map +0 -1
- package/dist/shared/email/types.d.ts +0 -113
- package/dist/shared/email/types.d.ts.map +0 -1
- package/dist/shared/email/types.js +0 -7
- package/dist/shared/email/types.js.map +0 -1
- package/dist/shared/email/webhookRoutes.d.ts +0 -29
- package/dist/shared/email/webhookRoutes.d.ts.map +0 -1
- package/dist/shared/email/webhookRoutes.js +0 -125
- package/dist/shared/email/webhookRoutes.js.map +0 -1
- package/dist/shared/mcp/index.d.ts +0 -51
- package/dist/shared/mcp/index.d.ts.map +0 -1
- package/dist/shared/mcp/index.js +0 -51
- package/dist/shared/mcp/index.js.map +0 -1
- package/dist/shared/mcp/mcp-auth-routes.d.ts +0 -26
- package/dist/shared/mcp/mcp-auth-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-auth-routes.js +0 -141
- package/dist/shared/mcp/mcp-auth-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-db.d.ts +0 -99
- package/dist/shared/mcp/mcp-db.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-db.js +0 -106
- package/dist/shared/mcp/mcp-db.js.map +0 -1
- package/dist/shared/mcp/mcp-routes.d.ts +0 -29
- package/dist/shared/mcp/mcp-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-routes.js +0 -171
- package/dist/shared/mcp/mcp-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-tokens-routes.d.ts +0 -35
- package/dist/shared/mcp/mcp-tokens-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-tokens-routes.js +0 -94
- package/dist/shared/mcp/mcp-tokens-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-usage-routes.d.ts +0 -17
- package/dist/shared/mcp/mcp-usage-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-usage-routes.js +0 -81
- package/dist/shared/mcp/mcp-usage-routes.js.map +0 -1
- package/dist/shared/mcp/tenant-context.d.ts +0 -59
- package/dist/shared/mcp/tenant-context.d.ts.map +0 -1
- package/dist/shared/mcp/tenant-context.js +0 -136
- package/dist/shared/mcp/tenant-context.js.map +0 -1
- package/dist/shared/mcp/types.d.ts +0 -74
- package/dist/shared/mcp/types.d.ts.map +0 -1
- package/dist/shared/mcp/types.js +0 -7
- package/dist/shared/mcp/types.js.map +0 -1
- package/dist/shared/planner/GoogleCalendarService.d.ts +0 -137
- package/dist/shared/planner/GoogleCalendarService.d.ts.map +0 -1
- package/dist/shared/planner/GoogleCalendarService.js +0 -525
- package/dist/shared/planner/GoogleCalendarService.js.map +0 -1
- package/dist/shared/planner/PlannerService.d.ts +0 -264
- package/dist/shared/planner/PlannerService.d.ts.map +0 -1
- package/dist/shared/planner/PlannerService.js +0 -1393
- package/dist/shared/planner/PlannerService.js.map +0 -1
- package/dist/shared/planner/index.d.ts +0 -37
- package/dist/shared/planner/index.d.ts.map +0 -1
- package/dist/shared/planner/index.js +0 -35
- package/dist/shared/planner/index.js.map +0 -1
- package/dist/shared/planner/intervals.d.ts +0 -60
- package/dist/shared/planner/intervals.d.ts.map +0 -1
- package/dist/shared/planner/intervals.js +0 -141
- package/dist/shared/planner/intervals.js.map +0 -1
- package/dist/shared/planner/routes.d.ts +0 -69
- package/dist/shared/planner/routes.d.ts.map +0 -1
- package/dist/shared/planner/routes.js +0 -770
- package/dist/shared/planner/routes.js.map +0 -1
- package/dist/shared/planner/types.d.ts +0 -328
- package/dist/shared/planner/types.d.ts.map +0 -1
- package/dist/shared/planner/types.js +0 -9
- package/dist/shared/planner/types.js.map +0 -1
- package/dist/shared/storage/ImageProcessingService.d.ts +0 -32
- package/dist/shared/storage/ImageProcessingService.d.ts.map +0 -1
- package/dist/shared/storage/ImageProcessingService.js +0 -127
- package/dist/shared/storage/ImageProcessingService.js.map +0 -1
- package/dist/shared/storage/StorageProxyService.d.ts +0 -47
- package/dist/shared/storage/StorageProxyService.d.ts.map +0 -1
- package/dist/shared/storage/StorageProxyService.js +0 -196
- package/dist/shared/storage/StorageProxyService.js.map +0 -1
- package/dist/shared/storage/StorageUploadService.d.ts +0 -126
- package/dist/shared/storage/StorageUploadService.d.ts.map +0 -1
- package/dist/shared/storage/StorageUploadService.js +0 -206
- package/dist/shared/storage/StorageUploadService.js.map +0 -1
- package/dist/shared/storage/creative-urls.d.ts +0 -14
- package/dist/shared/storage/creative-urls.d.ts.map +0 -1
- package/dist/shared/storage/creative-urls.js +0 -30
- package/dist/shared/storage/creative-urls.js.map +0 -1
- package/dist/shared/storage/index.d.ts +0 -28
- package/dist/shared/storage/index.d.ts.map +0 -1
- package/dist/shared/storage/index.js +0 -27
- package/dist/shared/storage/index.js.map +0 -1
- package/dist/shared/storage/routes.d.ts +0 -42
- package/dist/shared/storage/routes.d.ts.map +0 -1
- package/dist/shared/storage/routes.js +0 -160
- package/dist/shared/storage/routes.js.map +0 -1
- package/dist/shared/storage/types.d.ts +0 -53
- package/dist/shared/storage/types.d.ts.map +0 -1
- package/dist/shared/storage/types.js +0 -2
- package/dist/shared/storage/types.js.map +0 -1
- package/dist/shared/telegram/index.d.ts +0 -4
- package/dist/shared/telegram/index.d.ts.map +0 -1
- package/dist/shared/telegram/index.js +0 -3
- package/dist/shared/telegram/index.js.map +0 -1
- package/dist/shared/telegram/routes.d.ts +0 -43
- package/dist/shared/telegram/routes.d.ts.map +0 -1
- package/dist/shared/telegram/routes.js +0 -868
- package/dist/shared/telegram/routes.js.map +0 -1
- package/dist/shared/telegram/types.d.ts +0 -168
- package/dist/shared/telegram/types.d.ts.map +0 -1
- package/dist/shared/telegram/types.js +0 -7
- package/dist/shared/telegram/types.js.map +0 -1
- package/dist/shared/telegram/utils.d.ts +0 -44
- package/dist/shared/telegram/utils.d.ts.map +0 -1
- package/dist/shared/telegram/utils.js +0 -121
- package/dist/shared/telegram/utils.js.map +0 -1
- package/dist/storage.d.ts +0 -9
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -8
- package/dist/storage.js.map +0 -1
- package/dist/telemetry.d.ts +0 -9
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -8
- package/dist/telemetry.js.map +0 -1
- package/scripts/postinstall.js +0 -79
- package/src/shared/affiliate/migrations/001_create_affiliates.sql +0 -49
- package/src/shared/affiliate/migrations/002_create_affiliate_commissions.sql +0 -31
- package/src/shared/affiliate/migrations/003_create_affiliate_clicks.sql +0 -26
- package/src/shared/affiliate/migrations/004_create_affiliate_payments.sql +0 -34
- package/src/shared/affiliate/migrations/005_create_affiliate_tier_history.sql +0 -19
- package/src/shared/affiliate/migrations/006_create_affiliate_rpc_functions.sql +0 -209
- package/src/shared/affiliate/migrations/007_create_affiliate_rls_policies.sql +0 -123
- package/src/shared/billing/migrations/00000000000001_billing.sql +0 -114
- package/src/shared/email/migrations/000_create_email_logs.sql +0 -27
- package/src/shared/email/migrations/001_create_email_templates.sql +0 -27
- package/src/shared/email/migrations/002_add_rls_baseline_policies.sql +0 -37
- package/src/shared/email/migrations/003_create_gmail_accounts.sql +0 -82
- package/src/shared/email/migrations/004_add_email_logs_tracking_columns.sql +0 -15
- package/src/shared/mcp/migrations/001_mcp_api_tokens.sql +0 -21
- package/src/shared/mcp/migrations/002_mcp_audit_log.sql +0 -16
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BillingService — Stripe + Mollie dual-provider billing
|
|
3
|
-
*
|
|
4
|
-
* Handles:
|
|
5
|
-
* - Customer creation/lookup (Stripe + Mollie)
|
|
6
|
-
* - Checkout session creation (subscription mode)
|
|
7
|
-
* - Stripe Customer Portal
|
|
8
|
-
* - Invoice retrieval
|
|
9
|
-
* - Idempotent event logging
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* ```typescript
|
|
13
|
-
* import { BillingService } from '@soulbatical/tetra-core';
|
|
14
|
-
*
|
|
15
|
-
* const billing = new BillingService(config);
|
|
16
|
-
* const { url } = await billing.createStripeCheckout(orgId, 'pro', 'monthly');
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
import type { BillingConfig, BillingCycle, PlanConfig } from './types.js';
|
|
20
|
-
export declare class BillingService {
|
|
21
|
-
private config;
|
|
22
|
-
private stripeClient;
|
|
23
|
-
private mollieClient;
|
|
24
|
-
constructor(config: BillingConfig);
|
|
25
|
-
private getStripe;
|
|
26
|
-
private getMollie;
|
|
27
|
-
getPlan(key: string): PlanConfig | undefined;
|
|
28
|
-
get isStripeConfigured(): boolean;
|
|
29
|
-
get isMollieConfigured(): boolean;
|
|
30
|
-
getOrCreateStripeCustomer(orgId: string): Promise<string>;
|
|
31
|
-
getOrCreateMollieCustomer(orgId: string): Promise<string>;
|
|
32
|
-
createStripeCheckout(orgId: string, plan: string, cycle: BillingCycle): Promise<{
|
|
33
|
-
url: string;
|
|
34
|
-
}>;
|
|
35
|
-
createMollieCheckout(orgId: string, plan: string, cycle: BillingCycle): Promise<{
|
|
36
|
-
url: string;
|
|
37
|
-
}>;
|
|
38
|
-
createStripePortal(orgId: string, returnPath?: string): Promise<{
|
|
39
|
-
url: string;
|
|
40
|
-
}>;
|
|
41
|
-
getInvoices(orgId: string, limit?: number): Promise<any[]>;
|
|
42
|
-
handleStripeWebhook(rawBody: Buffer, signature: string): Promise<void>;
|
|
43
|
-
handleMollieWebhook(paymentId: string): Promise<void>;
|
|
44
|
-
private handleStripeCheckoutCompleted;
|
|
45
|
-
private handleStripeSubscriptionUpdated;
|
|
46
|
-
private handleStripeSubscriptionDeleted;
|
|
47
|
-
private handleStripePaymentFailed;
|
|
48
|
-
private handleStripePaymentSucceeded;
|
|
49
|
-
private createMollieSubscription;
|
|
50
|
-
private renewMollieSubscription;
|
|
51
|
-
private handleMolliePaymentFailed;
|
|
52
|
-
private assertNoActiveSubscription;
|
|
53
|
-
private logEvent;
|
|
54
|
-
private extractStripeSubscriptionId;
|
|
55
|
-
}
|
|
56
|
-
//# sourceMappingURL=BillingService.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"BillingService.d.ts","sourceRoot":"","sources":["../../../src/shared/billing/BillingService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAmB,UAAU,EAAE,MAAM,YAAY,CAAC;AAK3F,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,YAAY,CAAa;gBAErB,MAAM,EAAE,aAAa;YAMnB,SAAS;YAWT,SAAS;IAavB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAI5C,IAAI,kBAAkB,IAAI,OAAO,CAEhC;IAED,IAAI,kBAAkB,IAAI,OAAO,CAEhC;IAIK,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyBzD,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgCzD,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAiChG,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IA+BhG,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAqBhF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IA+BtD,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCtE,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YA+B7C,6BAA6B;YA8D7B,+BAA+B;YAsD/B,+BAA+B;YAmC/B,yBAAyB;YA8BzB,4BAA4B;YA+B5B,wBAAwB;YA+CxB,uBAAuB;YAoCvB,yBAAyB;YAqCzB,0BAA0B;YAe1B,QAAQ;IAyBtB,OAAO,CAAC,2BAA2B;CAQpC"}
|
|
@@ -1,588 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BillingService — Stripe + Mollie dual-provider billing
|
|
3
|
-
*
|
|
4
|
-
* Handles:
|
|
5
|
-
* - Customer creation/lookup (Stripe + Mollie)
|
|
6
|
-
* - Checkout session creation (subscription mode)
|
|
7
|
-
* - Stripe Customer Portal
|
|
8
|
-
* - Invoice retrieval
|
|
9
|
-
* - Idempotent event logging
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* ```typescript
|
|
13
|
-
* import { BillingService } from '@soulbatical/tetra-core';
|
|
14
|
-
*
|
|
15
|
-
* const billing = new BillingService(config);
|
|
16
|
-
* const { url } = await billing.createStripeCheckout(orgId, 'pro', 'monthly');
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
import { createLogger } from '../../utils/logger.js';
|
|
20
|
-
const logger = createLogger('billing:service');
|
|
21
|
-
export class BillingService {
|
|
22
|
-
config;
|
|
23
|
-
stripeClient = null;
|
|
24
|
-
mollieClient = null;
|
|
25
|
-
constructor(config) {
|
|
26
|
-
this.config = config;
|
|
27
|
-
}
|
|
28
|
-
// ─── Lazy client initialization ──────────────────────────
|
|
29
|
-
async getStripe() {
|
|
30
|
-
if (!this.config.stripe.secretKey) {
|
|
31
|
-
throw new Error('Stripe not configured');
|
|
32
|
-
}
|
|
33
|
-
if (!this.stripeClient) {
|
|
34
|
-
const stripe = (await import('stripe')).default;
|
|
35
|
-
this.stripeClient = new stripe(this.config.stripe.secretKey);
|
|
36
|
-
}
|
|
37
|
-
return this.stripeClient;
|
|
38
|
-
}
|
|
39
|
-
async getMollie() {
|
|
40
|
-
if (!this.config.mollie.apiKey) {
|
|
41
|
-
throw new Error('Mollie not configured');
|
|
42
|
-
}
|
|
43
|
-
if (!this.mollieClient) {
|
|
44
|
-
const { createMollieClient } = await import('@mollie/api-client');
|
|
45
|
-
this.mollieClient = createMollieClient({ apiKey: this.config.mollie.apiKey });
|
|
46
|
-
}
|
|
47
|
-
return this.mollieClient;
|
|
48
|
-
}
|
|
49
|
-
// ─── Plan helpers ─────────────────────────────────────────
|
|
50
|
-
getPlan(key) {
|
|
51
|
-
return this.config.plans[key];
|
|
52
|
-
}
|
|
53
|
-
get isStripeConfigured() {
|
|
54
|
-
return !!(this.config.stripe.secretKey && this.config.stripe.webhookSecret);
|
|
55
|
-
}
|
|
56
|
-
get isMollieConfigured() {
|
|
57
|
-
return !!this.config.mollie.apiKey;
|
|
58
|
-
}
|
|
59
|
-
// ─── Customer management ──────────────────────────────────
|
|
60
|
-
async getOrCreateStripeCustomer(orgId) {
|
|
61
|
-
const db = this.config.getSystemDB();
|
|
62
|
-
const { data: org } = await db
|
|
63
|
-
.from('organizations')
|
|
64
|
-
.select('id, name, contact_email, stripe_customer_id')
|
|
65
|
-
.eq('id', orgId)
|
|
66
|
-
.single();
|
|
67
|
-
if (org?.stripe_customer_id)
|
|
68
|
-
return org.stripe_customer_id;
|
|
69
|
-
const stripe = await this.getStripe();
|
|
70
|
-
const customer = await stripe.customers.create({
|
|
71
|
-
name: org?.name || undefined,
|
|
72
|
-
email: org?.contact_email || undefined,
|
|
73
|
-
metadata: { organization_id: orgId },
|
|
74
|
-
});
|
|
75
|
-
await db
|
|
76
|
-
.from('organizations')
|
|
77
|
-
.update({ stripe_customer_id: customer.id })
|
|
78
|
-
.eq('id', orgId);
|
|
79
|
-
return customer.id;
|
|
80
|
-
}
|
|
81
|
-
async getOrCreateMollieCustomer(orgId) {
|
|
82
|
-
const db = this.config.getSystemDB();
|
|
83
|
-
// Check existing via subscription
|
|
84
|
-
const { data: existingSub } = await db
|
|
85
|
-
.from('subscriptions')
|
|
86
|
-
.select('external_customer_id')
|
|
87
|
-
.eq('organization_id', orgId)
|
|
88
|
-
.eq('provider', 'mollie')
|
|
89
|
-
.limit(1)
|
|
90
|
-
.single();
|
|
91
|
-
if (existingSub?.external_customer_id)
|
|
92
|
-
return existingSub.external_customer_id;
|
|
93
|
-
const { data: org } = await db
|
|
94
|
-
.from('organizations')
|
|
95
|
-
.select('id, name, contact_email')
|
|
96
|
-
.eq('id', orgId)
|
|
97
|
-
.single();
|
|
98
|
-
const mollie = await this.getMollie();
|
|
99
|
-
const customer = await mollie.customers.create({
|
|
100
|
-
name: org?.name || 'Organization',
|
|
101
|
-
email: org?.contact_email || undefined,
|
|
102
|
-
metadata: JSON.stringify({ organization_id: orgId }),
|
|
103
|
-
});
|
|
104
|
-
return customer.id;
|
|
105
|
-
}
|
|
106
|
-
// ─── Checkout ─────────────────────────────────────────────
|
|
107
|
-
async createStripeCheckout(orgId, plan, cycle) {
|
|
108
|
-
const planConfig = this.getPlan(plan);
|
|
109
|
-
if (!planConfig)
|
|
110
|
-
throw new Error(`Invalid plan: ${plan}`);
|
|
111
|
-
const priceId = cycle === 'yearly' ? planConfig.stripePriceYearly : planConfig.stripePriceMonthly;
|
|
112
|
-
if (!priceId)
|
|
113
|
-
throw new Error(`No Stripe price configured for ${plan} (${cycle})`);
|
|
114
|
-
// Check existing active subscription
|
|
115
|
-
await this.assertNoActiveSubscription(orgId);
|
|
116
|
-
const customerId = await this.getOrCreateStripeCustomer(orgId);
|
|
117
|
-
const stripe = await this.getStripe();
|
|
118
|
-
const sessionParams = {
|
|
119
|
-
customer: customerId,
|
|
120
|
-
mode: 'subscription',
|
|
121
|
-
line_items: [{ price: priceId, quantity: 1 }],
|
|
122
|
-
success_url: `${this.config.frontendUrl}${this.config.successPath}`,
|
|
123
|
-
cancel_url: `${this.config.frontendUrl}${this.config.cancelPath}`,
|
|
124
|
-
metadata: { org_id: orgId, plan, cycle },
|
|
125
|
-
};
|
|
126
|
-
if (this.config.trialDays > 0) {
|
|
127
|
-
sessionParams.subscription_data = {
|
|
128
|
-
trial_period_days: this.config.trialDays,
|
|
129
|
-
metadata: { org_id: orgId, plan, cycle },
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
const session = await stripe.checkout.sessions.create(sessionParams);
|
|
133
|
-
return { url: session.url };
|
|
134
|
-
}
|
|
135
|
-
async createMollieCheckout(orgId, plan, cycle) {
|
|
136
|
-
const planConfig = this.getPlan(plan);
|
|
137
|
-
if (!planConfig)
|
|
138
|
-
throw new Error(`Invalid plan: ${plan}`);
|
|
139
|
-
// Check existing active subscription
|
|
140
|
-
await this.assertNoActiveSubscription(orgId);
|
|
141
|
-
const amount = cycle === 'yearly' ? planConfig.mollieAmountYearly : planConfig.mollieAmountMonthly;
|
|
142
|
-
const customerId = await this.getOrCreateMollieCustomer(orgId);
|
|
143
|
-
const mollie = await this.getMollie();
|
|
144
|
-
const { PaymentMethod, SequenceType } = await import('@mollie/api-client');
|
|
145
|
-
const webhookUrl = `${this.config.mollie.webhookBaseUrl}/api/billing/webhook/mollie`;
|
|
146
|
-
const payment = await mollie.payments.create({
|
|
147
|
-
amount: { currency: 'EUR', value: amount },
|
|
148
|
-
description: `${this.config.productName} ${planConfig.name} (${cycle})`,
|
|
149
|
-
redirectUrl: `${this.config.frontendUrl}${this.config.successPath}`,
|
|
150
|
-
webhookUrl,
|
|
151
|
-
metadata: JSON.stringify({ org_id: orgId, plan, cycle }),
|
|
152
|
-
customerId,
|
|
153
|
-
method: PaymentMethod.ideal,
|
|
154
|
-
sequenceType: SequenceType.first,
|
|
155
|
-
});
|
|
156
|
-
return { url: payment.getCheckoutUrl() };
|
|
157
|
-
}
|
|
158
|
-
// ─── Portal & Invoices ────────────────────────────────────
|
|
159
|
-
async createStripePortal(orgId, returnPath) {
|
|
160
|
-
const db = this.config.getSystemDB();
|
|
161
|
-
const { data: org } = await db
|
|
162
|
-
.from('organizations')
|
|
163
|
-
.select('stripe_customer_id')
|
|
164
|
-
.eq('id', orgId)
|
|
165
|
-
.single();
|
|
166
|
-
if (!org?.stripe_customer_id) {
|
|
167
|
-
throw new Error('No Stripe customer found. Subscribe first.');
|
|
168
|
-
}
|
|
169
|
-
const stripe = await this.getStripe();
|
|
170
|
-
const session = await stripe.billingPortal.sessions.create({
|
|
171
|
-
customer: org.stripe_customer_id,
|
|
172
|
-
return_url: `${this.config.frontendUrl}${returnPath || this.config.successPath}`,
|
|
173
|
-
});
|
|
174
|
-
return { url: session.url };
|
|
175
|
-
}
|
|
176
|
-
async getInvoices(orgId, limit = 24) {
|
|
177
|
-
const db = this.config.getSystemDB();
|
|
178
|
-
const { data: org } = await db
|
|
179
|
-
.from('organizations')
|
|
180
|
-
.select('stripe_customer_id')
|
|
181
|
-
.eq('id', orgId)
|
|
182
|
-
.single();
|
|
183
|
-
if (!org?.stripe_customer_id)
|
|
184
|
-
return [];
|
|
185
|
-
const stripe = await this.getStripe();
|
|
186
|
-
const invoices = await stripe.invoices.list({
|
|
187
|
-
customer: org.stripe_customer_id,
|
|
188
|
-
limit,
|
|
189
|
-
});
|
|
190
|
-
return invoices.data.map((inv) => ({
|
|
191
|
-
id: inv.id,
|
|
192
|
-
number: inv.number,
|
|
193
|
-
status: inv.status,
|
|
194
|
-
amount_due: inv.amount_due,
|
|
195
|
-
amount_paid: inv.amount_paid,
|
|
196
|
-
currency: inv.currency,
|
|
197
|
-
created: inv.created,
|
|
198
|
-
hosted_invoice_url: inv.hosted_invoice_url,
|
|
199
|
-
invoice_pdf: inv.invoice_pdf,
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
// ─── Webhook Processing ───────────────────────────────────
|
|
203
|
-
async handleStripeWebhook(rawBody, signature) {
|
|
204
|
-
const stripe = await this.getStripe();
|
|
205
|
-
let event;
|
|
206
|
-
try {
|
|
207
|
-
event = stripe.webhooks.constructEvent(rawBody, signature, this.config.stripe.webhookSecret);
|
|
208
|
-
}
|
|
209
|
-
catch (err) {
|
|
210
|
-
logger.error({ error: err.message }, 'Stripe signature verification failed');
|
|
211
|
-
throw new Error('Invalid signature');
|
|
212
|
-
}
|
|
213
|
-
const db = this.config.getWebhookDB('stripe');
|
|
214
|
-
logger.info({ type: event.type, id: event.id }, 'Stripe webhook received');
|
|
215
|
-
switch (event.type) {
|
|
216
|
-
case 'checkout.session.completed':
|
|
217
|
-
await this.handleStripeCheckoutCompleted(event, db);
|
|
218
|
-
break;
|
|
219
|
-
case 'customer.subscription.updated':
|
|
220
|
-
await this.handleStripeSubscriptionUpdated(event, db);
|
|
221
|
-
break;
|
|
222
|
-
case 'customer.subscription.deleted':
|
|
223
|
-
await this.handleStripeSubscriptionDeleted(event, db);
|
|
224
|
-
break;
|
|
225
|
-
case 'invoice.payment_failed':
|
|
226
|
-
await this.handleStripePaymentFailed(event, db);
|
|
227
|
-
break;
|
|
228
|
-
case 'invoice.payment_succeeded':
|
|
229
|
-
await this.handleStripePaymentSucceeded(event, db);
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async handleMollieWebhook(paymentId) {
|
|
234
|
-
if (!paymentId)
|
|
235
|
-
return;
|
|
236
|
-
const mollie = await this.getMollie();
|
|
237
|
-
const payment = await mollie.payments.get(paymentId);
|
|
238
|
-
const metadata = payment.metadata ? JSON.parse(payment.metadata) : {};
|
|
239
|
-
const orgId = metadata.org_id;
|
|
240
|
-
const plan = metadata.plan;
|
|
241
|
-
const cycle = metadata.cycle;
|
|
242
|
-
if (!orgId)
|
|
243
|
-
return;
|
|
244
|
-
const db = this.config.getWebhookDB('mollie');
|
|
245
|
-
const { PaymentStatus } = await import('@mollie/api-client');
|
|
246
|
-
if (payment.status === PaymentStatus.paid) {
|
|
247
|
-
const paymentAny = payment;
|
|
248
|
-
const isFirstPayment = paymentAny.sequenceType === 'first';
|
|
249
|
-
if (isFirstPayment && plan) {
|
|
250
|
-
await this.createMollieSubscription(orgId, plan, cycle || 'monthly', paymentAny, paymentId, db);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
await this.renewMollieSubscription(orgId, paymentId, db);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else if (payment.status === PaymentStatus.failed || payment.status === PaymentStatus.expired) {
|
|
257
|
-
await this.handleMolliePaymentFailed(orgId, paymentId, payment.status, db);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// ─── Stripe Webhook Handlers (private) ────────────────────
|
|
261
|
-
async handleStripeCheckoutCompleted(event, db) {
|
|
262
|
-
const session = event.data.object;
|
|
263
|
-
const orgId = session.metadata?.org_id;
|
|
264
|
-
const plan = session.metadata?.plan;
|
|
265
|
-
const cycle = (session.metadata?.cycle || 'monthly');
|
|
266
|
-
if (!orgId || !plan)
|
|
267
|
-
return;
|
|
268
|
-
// Idempotency check
|
|
269
|
-
const isNew = await this.logEvent({
|
|
270
|
-
db,
|
|
271
|
-
organizationId: orgId,
|
|
272
|
-
eventType: 'created',
|
|
273
|
-
stripeEventId: event.id,
|
|
274
|
-
provider: 'stripe',
|
|
275
|
-
data: { plan, cycle, session_id: session.id },
|
|
276
|
-
});
|
|
277
|
-
if (!isNew)
|
|
278
|
-
return;
|
|
279
|
-
const stripeSubId = typeof session.subscription === 'string' ? session.subscription : null;
|
|
280
|
-
const customerId = typeof session.customer === 'string' ? session.customer : '';
|
|
281
|
-
const planConfig = this.getPlan(plan);
|
|
282
|
-
const priceCents = cycle === 'yearly' ? (planConfig?.priceYearCents || 0) : (planConfig?.priceMonthCents || 0);
|
|
283
|
-
const now = new Date();
|
|
284
|
-
const trialEnd = this.config.trialDays > 0
|
|
285
|
-
? new Date(now.getTime() + this.config.trialDays * 24 * 60 * 60 * 1000)
|
|
286
|
-
: null;
|
|
287
|
-
const periodEnd = new Date(now.getTime() + (cycle === 'yearly' ? 365 : 30) * 24 * 60 * 60 * 1000);
|
|
288
|
-
const { data: sub } = await db
|
|
289
|
-
.from('subscriptions')
|
|
290
|
-
.insert({
|
|
291
|
-
organization_id: orgId,
|
|
292
|
-
provider: 'stripe',
|
|
293
|
-
external_subscription_id: stripeSubId,
|
|
294
|
-
external_customer_id: customerId,
|
|
295
|
-
plan,
|
|
296
|
-
billing_cycle: cycle,
|
|
297
|
-
price_amount_cents: priceCents,
|
|
298
|
-
status: trialEnd ? 'trialing' : 'active',
|
|
299
|
-
trial_end: trialEnd?.toISOString() || null,
|
|
300
|
-
current_period_start: now.toISOString(),
|
|
301
|
-
current_period_end: periodEnd.toISOString(),
|
|
302
|
-
})
|
|
303
|
-
.select('id')
|
|
304
|
-
.single();
|
|
305
|
-
// Update org plan
|
|
306
|
-
await db.from('organizations').update({ plan }).eq('id', orgId);
|
|
307
|
-
// Update event with subscription_id
|
|
308
|
-
if (sub) {
|
|
309
|
-
await db
|
|
310
|
-
.from('subscription_events')
|
|
311
|
-
.update({ subscription_id: sub.id })
|
|
312
|
-
.eq('stripe_event_id', event.id);
|
|
313
|
-
}
|
|
314
|
-
logger.info({ orgId, plan, provider: 'stripe' }, 'Subscription created');
|
|
315
|
-
await this.config.onSubscriptionCreated?.(orgId, plan, 'stripe');
|
|
316
|
-
}
|
|
317
|
-
async handleStripeSubscriptionUpdated(event, db) {
|
|
318
|
-
const stripeSub = event.data.object;
|
|
319
|
-
const { data: sub } = await db
|
|
320
|
-
.from('subscriptions')
|
|
321
|
-
.select('id, organization_id')
|
|
322
|
-
.eq('external_subscription_id', stripeSub.id)
|
|
323
|
-
.single();
|
|
324
|
-
if (!sub)
|
|
325
|
-
return;
|
|
326
|
-
const isNew = await this.logEvent({
|
|
327
|
-
db,
|
|
328
|
-
organizationId: sub.organization_id,
|
|
329
|
-
subscriptionId: sub.id,
|
|
330
|
-
eventType: 'updated',
|
|
331
|
-
stripeEventId: event.id,
|
|
332
|
-
provider: 'stripe',
|
|
333
|
-
data: { status: stripeSub.status },
|
|
334
|
-
});
|
|
335
|
-
if (!isNew)
|
|
336
|
-
return;
|
|
337
|
-
const statusMap = {
|
|
338
|
-
active: 'active',
|
|
339
|
-
past_due: 'past_due',
|
|
340
|
-
canceled: 'canceled',
|
|
341
|
-
incomplete_expired: 'canceled',
|
|
342
|
-
trialing: 'trialing',
|
|
343
|
-
unpaid: 'unpaid',
|
|
344
|
-
};
|
|
345
|
-
const updates = {
|
|
346
|
-
status: statusMap[stripeSub.status] || 'active',
|
|
347
|
-
updated_at: new Date().toISOString(),
|
|
348
|
-
};
|
|
349
|
-
// Update period dates
|
|
350
|
-
const item = stripeSub.items?.data?.[0];
|
|
351
|
-
if (item) {
|
|
352
|
-
const itemAny = item;
|
|
353
|
-
if (typeof itemAny.current_period_start === 'number') {
|
|
354
|
-
updates.current_period_start = new Date(itemAny.current_period_start * 1000).toISOString();
|
|
355
|
-
}
|
|
356
|
-
if (typeof itemAny.current_period_end === 'number') {
|
|
357
|
-
updates.current_period_end = new Date(itemAny.current_period_end * 1000).toISOString();
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (stripeSub.cancel_at)
|
|
361
|
-
updates.cancel_at = new Date(stripeSub.cancel_at * 1000).toISOString();
|
|
362
|
-
if (stripeSub.canceled_at)
|
|
363
|
-
updates.canceled_at = new Date(stripeSub.canceled_at * 1000).toISOString();
|
|
364
|
-
if (stripeSub.trial_end)
|
|
365
|
-
updates.trial_end = new Date(stripeSub.trial_end * 1000).toISOString();
|
|
366
|
-
await db.from('subscriptions').update(updates).eq('id', sub.id);
|
|
367
|
-
}
|
|
368
|
-
async handleStripeSubscriptionDeleted(event, db) {
|
|
369
|
-
const stripeSub = event.data.object;
|
|
370
|
-
const { data: sub } = await db
|
|
371
|
-
.from('subscriptions')
|
|
372
|
-
.select('id, organization_id')
|
|
373
|
-
.eq('external_subscription_id', stripeSub.id)
|
|
374
|
-
.single();
|
|
375
|
-
if (!sub)
|
|
376
|
-
return;
|
|
377
|
-
const isNew = await this.logEvent({
|
|
378
|
-
db,
|
|
379
|
-
organizationId: sub.organization_id,
|
|
380
|
-
subscriptionId: sub.id,
|
|
381
|
-
eventType: 'canceled',
|
|
382
|
-
stripeEventId: event.id,
|
|
383
|
-
provider: 'stripe',
|
|
384
|
-
});
|
|
385
|
-
if (!isNew)
|
|
386
|
-
return;
|
|
387
|
-
await db
|
|
388
|
-
.from('subscriptions')
|
|
389
|
-
.update({
|
|
390
|
-
status: 'canceled',
|
|
391
|
-
canceled_at: new Date().toISOString(),
|
|
392
|
-
ended_at: new Date().toISOString(),
|
|
393
|
-
updated_at: new Date().toISOString(),
|
|
394
|
-
})
|
|
395
|
-
.eq('id', sub.id);
|
|
396
|
-
await db.from('organizations').update({ plan: 'free' }).eq('id', sub.organization_id);
|
|
397
|
-
logger.info({ orgId: sub.organization_id }, 'Subscription canceled, downgraded to free');
|
|
398
|
-
await this.config.onSubscriptionCanceled?.(sub.organization_id);
|
|
399
|
-
}
|
|
400
|
-
async handleStripePaymentFailed(event, db) {
|
|
401
|
-
const invoice = event.data.object;
|
|
402
|
-
const subId = this.extractStripeSubscriptionId(invoice);
|
|
403
|
-
if (!subId)
|
|
404
|
-
return;
|
|
405
|
-
const { data: sub } = await db
|
|
406
|
-
.from('subscriptions')
|
|
407
|
-
.select('id, organization_id')
|
|
408
|
-
.eq('external_subscription_id', subId)
|
|
409
|
-
.single();
|
|
410
|
-
if (!sub)
|
|
411
|
-
return;
|
|
412
|
-
const isNew = await this.logEvent({
|
|
413
|
-
db,
|
|
414
|
-
organizationId: sub.organization_id,
|
|
415
|
-
subscriptionId: sub.id,
|
|
416
|
-
eventType: 'payment_failed',
|
|
417
|
-
stripeEventId: event.id,
|
|
418
|
-
provider: 'stripe',
|
|
419
|
-
});
|
|
420
|
-
if (!isNew)
|
|
421
|
-
return;
|
|
422
|
-
await db
|
|
423
|
-
.from('subscriptions')
|
|
424
|
-
.update({ status: 'past_due', updated_at: new Date().toISOString() })
|
|
425
|
-
.eq('id', sub.id);
|
|
426
|
-
await this.config.onPaymentFailed?.(sub.organization_id);
|
|
427
|
-
}
|
|
428
|
-
async handleStripePaymentSucceeded(event, db) {
|
|
429
|
-
const invoice = event.data.object;
|
|
430
|
-
const subId = this.extractStripeSubscriptionId(invoice);
|
|
431
|
-
if (!subId)
|
|
432
|
-
return;
|
|
433
|
-
const { data: sub } = await db
|
|
434
|
-
.from('subscriptions')
|
|
435
|
-
.select('id, organization_id')
|
|
436
|
-
.eq('external_subscription_id', subId)
|
|
437
|
-
.single();
|
|
438
|
-
if (!sub)
|
|
439
|
-
return;
|
|
440
|
-
const isNew = await this.logEvent({
|
|
441
|
-
db,
|
|
442
|
-
organizationId: sub.organization_id,
|
|
443
|
-
subscriptionId: sub.id,
|
|
444
|
-
eventType: 'payment_succeeded',
|
|
445
|
-
stripeEventId: event.id,
|
|
446
|
-
provider: 'stripe',
|
|
447
|
-
data: { amount: invoice.amount_paid },
|
|
448
|
-
});
|
|
449
|
-
if (!isNew)
|
|
450
|
-
return;
|
|
451
|
-
await db
|
|
452
|
-
.from('subscriptions')
|
|
453
|
-
.update({ status: 'active', updated_at: new Date().toISOString() })
|
|
454
|
-
.eq('id', sub.id);
|
|
455
|
-
}
|
|
456
|
-
// ─── Mollie Webhook Handlers (private) ────────────────────
|
|
457
|
-
async createMollieSubscription(orgId, plan, cycle, paymentAny, paymentId, db) {
|
|
458
|
-
const mandateId = paymentAny.mandateId;
|
|
459
|
-
const customerId = typeof paymentAny.customerId === 'string' ? paymentAny.customerId : '';
|
|
460
|
-
const planConfig = this.getPlan(plan);
|
|
461
|
-
const priceCents = cycle === 'yearly' ? (planConfig?.priceYearCents || 0) : (planConfig?.priceMonthCents || 0);
|
|
462
|
-
const now = new Date();
|
|
463
|
-
const periodEnd = new Date(now.getTime() + (cycle === 'yearly' ? 365 : 30) * 24 * 60 * 60 * 1000);
|
|
464
|
-
const { data: sub } = await db
|
|
465
|
-
.from('subscriptions')
|
|
466
|
-
.insert({
|
|
467
|
-
organization_id: orgId,
|
|
468
|
-
provider: 'mollie',
|
|
469
|
-
external_subscription_id: mandateId || paymentId,
|
|
470
|
-
external_customer_id: customerId,
|
|
471
|
-
plan,
|
|
472
|
-
billing_cycle: cycle,
|
|
473
|
-
price_amount_cents: priceCents,
|
|
474
|
-
status: 'active',
|
|
475
|
-
current_period_start: now.toISOString(),
|
|
476
|
-
current_period_end: periodEnd.toISOString(),
|
|
477
|
-
})
|
|
478
|
-
.select('id')
|
|
479
|
-
.single();
|
|
480
|
-
await this.logEvent({
|
|
481
|
-
db,
|
|
482
|
-
organizationId: orgId,
|
|
483
|
-
subscriptionId: sub?.id,
|
|
484
|
-
eventType: 'created',
|
|
485
|
-
provider: 'mollie',
|
|
486
|
-
data: { plan, cycle, payment_id: paymentId, mandate_id: mandateId },
|
|
487
|
-
});
|
|
488
|
-
await db.from('organizations').update({ plan }).eq('id', orgId);
|
|
489
|
-
logger.info({ orgId, plan, provider: 'mollie' }, 'Subscription created');
|
|
490
|
-
await this.config.onSubscriptionCreated?.(orgId, plan, 'mollie');
|
|
491
|
-
}
|
|
492
|
-
async renewMollieSubscription(orgId, paymentId, db) {
|
|
493
|
-
const { data: sub } = await db
|
|
494
|
-
.from('subscriptions')
|
|
495
|
-
.select('id, billing_cycle')
|
|
496
|
-
.eq('organization_id', orgId)
|
|
497
|
-
.eq('provider', 'mollie')
|
|
498
|
-
.neq('status', 'canceled')
|
|
499
|
-
.order('created_at', { ascending: false })
|
|
500
|
-
.limit(1)
|
|
501
|
-
.single();
|
|
502
|
-
if (!sub)
|
|
503
|
-
return;
|
|
504
|
-
const now = new Date();
|
|
505
|
-
const periodEnd = new Date(now.getTime() + (sub.billing_cycle === 'yearly' ? 365 : 30) * 24 * 60 * 60 * 1000);
|
|
506
|
-
await db
|
|
507
|
-
.from('subscriptions')
|
|
508
|
-
.update({
|
|
509
|
-
status: 'active',
|
|
510
|
-
current_period_start: now.toISOString(),
|
|
511
|
-
current_period_end: periodEnd.toISOString(),
|
|
512
|
-
updated_at: now.toISOString(),
|
|
513
|
-
})
|
|
514
|
-
.eq('id', sub.id);
|
|
515
|
-
await this.logEvent({
|
|
516
|
-
db,
|
|
517
|
-
organizationId: orgId,
|
|
518
|
-
subscriptionId: sub.id,
|
|
519
|
-
eventType: 'renewed',
|
|
520
|
-
provider: 'mollie',
|
|
521
|
-
data: { payment_id: paymentId },
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
async handleMolliePaymentFailed(orgId, paymentId, status, db) {
|
|
525
|
-
const { data: sub } = await db
|
|
526
|
-
.from('subscriptions')
|
|
527
|
-
.select('id')
|
|
528
|
-
.eq('organization_id', orgId)
|
|
529
|
-
.eq('provider', 'mollie')
|
|
530
|
-
.neq('status', 'canceled')
|
|
531
|
-
.order('created_at', { ascending: false })
|
|
532
|
-
.limit(1)
|
|
533
|
-
.single();
|
|
534
|
-
if (!sub)
|
|
535
|
-
return;
|
|
536
|
-
await db
|
|
537
|
-
.from('subscriptions')
|
|
538
|
-
.update({ status: 'past_due', updated_at: new Date().toISOString() })
|
|
539
|
-
.eq('id', sub.id);
|
|
540
|
-
await this.logEvent({
|
|
541
|
-
db,
|
|
542
|
-
organizationId: orgId,
|
|
543
|
-
subscriptionId: sub.id,
|
|
544
|
-
eventType: 'payment_failed',
|
|
545
|
-
provider: 'mollie',
|
|
546
|
-
data: { payment_id: paymentId, status },
|
|
547
|
-
});
|
|
548
|
-
await this.config.onPaymentFailed?.(orgId);
|
|
549
|
-
}
|
|
550
|
-
// ─── Helpers ──────────────────────────────────────────────
|
|
551
|
-
async assertNoActiveSubscription(orgId) {
|
|
552
|
-
const db = this.config.getSystemDB();
|
|
553
|
-
const { data: existingSub } = await db
|
|
554
|
-
.from('subscriptions')
|
|
555
|
-
.select('id')
|
|
556
|
-
.eq('organization_id', orgId)
|
|
557
|
-
.neq('status', 'canceled')
|
|
558
|
-
.limit(1)
|
|
559
|
-
.single();
|
|
560
|
-
if (existingSub) {
|
|
561
|
-
throw new Error('Organization already has an active subscription.');
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
async logEvent(params) {
|
|
565
|
-
const { error } = await params.db.from('subscription_events').insert({
|
|
566
|
-
organization_id: params.organizationId,
|
|
567
|
-
subscription_id: params.subscriptionId || null,
|
|
568
|
-
event_type: params.eventType,
|
|
569
|
-
stripe_event_id: params.stripeEventId || null,
|
|
570
|
-
provider: params.provider,
|
|
571
|
-
data: params.data || {},
|
|
572
|
-
source: 'webhook',
|
|
573
|
-
});
|
|
574
|
-
// unique constraint violation = already processed (idempotent)
|
|
575
|
-
if (error?.code === '23505')
|
|
576
|
-
return false;
|
|
577
|
-
if (error)
|
|
578
|
-
logger.error({ error }, 'logEvent error');
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
extractStripeSubscriptionId(invoice) {
|
|
582
|
-
const invoiceAny = invoice;
|
|
583
|
-
const parentAny = (invoiceAny.parent ?? {});
|
|
584
|
-
return ((typeof invoiceAny.subscription === 'string' ? invoiceAny.subscription : null) ||
|
|
585
|
-
(typeof parentAny.subscription === 'string' ? parentAny.subscription : null));
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
//# sourceMappingURL=BillingService.js.map
|