@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.
Files changed (271) hide show
  1. package/README.md +78 -38
  2. package/dist/core/createApp.d.ts +1 -1
  3. package/dist/core/createApp.d.ts.map +1 -1
  4. package/dist/core/createApp.js +77 -2
  5. package/dist/core/createApp.js.map +1 -1
  6. package/dist/core/dualWriteProxy.d.ts +7 -2
  7. package/dist/core/dualWriteProxy.d.ts.map +1 -1
  8. package/dist/core/dualWriteProxy.js +16 -5
  9. package/dist/core/dualWriteProxy.js.map +1 -1
  10. package/dist/core/routeContext.d.ts +24 -0
  11. package/dist/core/routeContext.d.ts.map +1 -1
  12. package/dist/core/routeContext.js +31 -4
  13. package/dist/core/routeContext.js.map +1 -1
  14. package/dist/core/systemDb.d.ts +2 -2
  15. package/dist/core/systemDb.js +2 -2
  16. package/dist/generators/rls-checker.d.ts +1 -1
  17. package/dist/generators/rls-checker.js +1 -1
  18. package/dist/generators/rls-exec-sql.d.ts +1 -1
  19. package/dist/generators/rls-exec-sql.js +1 -1
  20. package/dist/generators/rpc/index.d.ts +1 -1
  21. package/dist/generators/rpc/index.js +1 -1
  22. package/dist/index.d.ts +3 -31
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -32
  25. package/dist/index.js.map +1 -1
  26. package/dist/middleware/securityMiddleware.d.ts +1 -1
  27. package/dist/middleware/securityMiddleware.d.ts.map +1 -1
  28. package/dist/middleware/validateBody.d.ts.map +1 -1
  29. package/dist/middleware/validateBody.js +51 -8
  30. package/dist/middleware/validateBody.js.map +1 -1
  31. package/dist/shared/rfc7807ErrorResponse.d.ts +7 -0
  32. package/dist/shared/rfc7807ErrorResponse.d.ts.map +1 -1
  33. package/dist/shared/rfc7807ErrorResponse.js +19 -5
  34. package/dist/shared/rfc7807ErrorResponse.js.map +1 -1
  35. package/dist/utils/logger.d.ts.map +1 -1
  36. package/dist/utils/logger.js +16 -1
  37. package/dist/utils/logger.js.map +1 -1
  38. package/package.json +33 -77
  39. package/dist/affiliate.d.ts +0 -11
  40. package/dist/affiliate.d.ts.map +0 -1
  41. package/dist/affiliate.js +0 -10
  42. package/dist/affiliate.js.map +0 -1
  43. package/dist/billing.d.ts +0 -8
  44. package/dist/billing.d.ts.map +0 -1
  45. package/dist/billing.js +0 -7
  46. package/dist/billing.js.map +0 -1
  47. package/dist/email.d.ts +0 -9
  48. package/dist/email.d.ts.map +0 -1
  49. package/dist/email.js +0 -8
  50. package/dist/email.js.map +0 -1
  51. package/dist/generators/rls-exec-sql.sql +0 -57
  52. package/dist/generators.d.ts +0 -15
  53. package/dist/generators.d.ts.map +0 -1
  54. package/dist/generators.js +0 -12
  55. package/dist/generators.js.map +0 -1
  56. package/dist/mcp.d.ts +0 -8
  57. package/dist/mcp.d.ts.map +0 -1
  58. package/dist/mcp.js +0 -7
  59. package/dist/mcp.js.map +0 -1
  60. package/dist/planner.d.ts +0 -8
  61. package/dist/planner.d.ts.map +0 -1
  62. package/dist/planner.js +0 -7
  63. package/dist/planner.js.map +0 -1
  64. package/dist/shared/affiliate/AffiliateAttributionService.d.ts +0 -47
  65. package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +0 -1
  66. package/dist/shared/affiliate/AffiliateAttributionService.js +0 -308
  67. package/dist/shared/affiliate/AffiliateAttributionService.js.map +0 -1
  68. package/dist/shared/affiliate/AffiliateClickService.d.ts +0 -35
  69. package/dist/shared/affiliate/AffiliateClickService.d.ts.map +0 -1
  70. package/dist/shared/affiliate/AffiliateClickService.js +0 -87
  71. package/dist/shared/affiliate/AffiliateClickService.js.map +0 -1
  72. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +0 -11
  73. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +0 -1
  74. package/dist/shared/affiliate/affiliateFeatureConfig.js +0 -242
  75. package/dist/shared/affiliate/affiliateFeatureConfig.js.map +0 -1
  76. package/dist/shared/affiliate/index.d.ts +0 -11
  77. package/dist/shared/affiliate/index.d.ts.map +0 -1
  78. package/dist/shared/affiliate/index.js +0 -13
  79. package/dist/shared/affiliate/index.js.map +0 -1
  80. package/dist/shared/affiliate/routes.d.ts +0 -87
  81. package/dist/shared/affiliate/routes.d.ts.map +0 -1
  82. package/dist/shared/affiliate/routes.js +0 -404
  83. package/dist/shared/affiliate/routes.js.map +0 -1
  84. package/dist/shared/affiliate/types.d.ts +0 -170
  85. package/dist/shared/affiliate/types.d.ts.map +0 -1
  86. package/dist/shared/affiliate/types.js +0 -11
  87. package/dist/shared/affiliate/types.js.map +0 -1
  88. package/dist/shared/billing/BillingService.d.ts +0 -56
  89. package/dist/shared/billing/BillingService.d.ts.map +0 -1
  90. package/dist/shared/billing/BillingService.js +0 -588
  91. package/dist/shared/billing/BillingService.js.map +0 -1
  92. package/dist/shared/billing/SeatBillingService.d.ts +0 -106
  93. package/dist/shared/billing/SeatBillingService.d.ts.map +0 -1
  94. package/dist/shared/billing/SeatBillingService.js +0 -292
  95. package/dist/shared/billing/SeatBillingService.js.map +0 -1
  96. package/dist/shared/billing/index.d.ts +0 -30
  97. package/dist/shared/billing/index.d.ts.map +0 -1
  98. package/dist/shared/billing/index.js +0 -27
  99. package/dist/shared/billing/index.js.map +0 -1
  100. package/dist/shared/billing/routes.d.ts +0 -45
  101. package/dist/shared/billing/routes.d.ts.map +0 -1
  102. package/dist/shared/billing/routes.js +0 -184
  103. package/dist/shared/billing/routes.js.map +0 -1
  104. package/dist/shared/billing/seat-pricing.d.ts +0 -53
  105. package/dist/shared/billing/seat-pricing.d.ts.map +0 -1
  106. package/dist/shared/billing/seat-pricing.js +0 -81
  107. package/dist/shared/billing/seat-pricing.js.map +0 -1
  108. package/dist/shared/billing/types.d.ts +0 -109
  109. package/dist/shared/billing/types.d.ts.map +0 -1
  110. package/dist/shared/billing/types.js +0 -8
  111. package/dist/shared/billing/types.js.map +0 -1
  112. package/dist/shared/email/EmailService.d.ts +0 -64
  113. package/dist/shared/email/EmailService.d.ts.map +0 -1
  114. package/dist/shared/email/EmailService.js +0 -300
  115. package/dist/shared/email/EmailService.js.map +0 -1
  116. package/dist/shared/email/adminRoutes.d.ts +0 -30
  117. package/dist/shared/email/adminRoutes.d.ts.map +0 -1
  118. package/dist/shared/email/adminRoutes.js +0 -227
  119. package/dist/shared/email/adminRoutes.js.map +0 -1
  120. package/dist/shared/email/gmail.d.ts +0 -208
  121. package/dist/shared/email/gmail.d.ts.map +0 -1
  122. package/dist/shared/email/gmail.js +0 -626
  123. package/dist/shared/email/gmail.js.map +0 -1
  124. package/dist/shared/email/index.d.ts +0 -15
  125. package/dist/shared/email/index.d.ts.map +0 -1
  126. package/dist/shared/email/index.js +0 -18
  127. package/dist/shared/email/index.js.map +0 -1
  128. package/dist/shared/email/mailgun.d.ts +0 -18
  129. package/dist/shared/email/mailgun.d.ts.map +0 -1
  130. package/dist/shared/email/mailgun.js +0 -76
  131. package/dist/shared/email/mailgun.js.map +0 -1
  132. package/dist/shared/email/sanitize.d.ts +0 -25
  133. package/dist/shared/email/sanitize.d.ts.map +0 -1
  134. package/dist/shared/email/sanitize.js +0 -39
  135. package/dist/shared/email/sanitize.js.map +0 -1
  136. package/dist/shared/email/smtp.d.ts +0 -20
  137. package/dist/shared/email/smtp.d.ts.map +0 -1
  138. package/dist/shared/email/smtp.js +0 -53
  139. package/dist/shared/email/smtp.js.map +0 -1
  140. package/dist/shared/email/types.d.ts +0 -113
  141. package/dist/shared/email/types.d.ts.map +0 -1
  142. package/dist/shared/email/types.js +0 -7
  143. package/dist/shared/email/types.js.map +0 -1
  144. package/dist/shared/email/webhookRoutes.d.ts +0 -29
  145. package/dist/shared/email/webhookRoutes.d.ts.map +0 -1
  146. package/dist/shared/email/webhookRoutes.js +0 -125
  147. package/dist/shared/email/webhookRoutes.js.map +0 -1
  148. package/dist/shared/mcp/index.d.ts +0 -51
  149. package/dist/shared/mcp/index.d.ts.map +0 -1
  150. package/dist/shared/mcp/index.js +0 -51
  151. package/dist/shared/mcp/index.js.map +0 -1
  152. package/dist/shared/mcp/mcp-auth-routes.d.ts +0 -26
  153. package/dist/shared/mcp/mcp-auth-routes.d.ts.map +0 -1
  154. package/dist/shared/mcp/mcp-auth-routes.js +0 -141
  155. package/dist/shared/mcp/mcp-auth-routes.js.map +0 -1
  156. package/dist/shared/mcp/mcp-db.d.ts +0 -99
  157. package/dist/shared/mcp/mcp-db.d.ts.map +0 -1
  158. package/dist/shared/mcp/mcp-db.js +0 -106
  159. package/dist/shared/mcp/mcp-db.js.map +0 -1
  160. package/dist/shared/mcp/mcp-routes.d.ts +0 -29
  161. package/dist/shared/mcp/mcp-routes.d.ts.map +0 -1
  162. package/dist/shared/mcp/mcp-routes.js +0 -171
  163. package/dist/shared/mcp/mcp-routes.js.map +0 -1
  164. package/dist/shared/mcp/mcp-tokens-routes.d.ts +0 -35
  165. package/dist/shared/mcp/mcp-tokens-routes.d.ts.map +0 -1
  166. package/dist/shared/mcp/mcp-tokens-routes.js +0 -94
  167. package/dist/shared/mcp/mcp-tokens-routes.js.map +0 -1
  168. package/dist/shared/mcp/mcp-usage-routes.d.ts +0 -17
  169. package/dist/shared/mcp/mcp-usage-routes.d.ts.map +0 -1
  170. package/dist/shared/mcp/mcp-usage-routes.js +0 -81
  171. package/dist/shared/mcp/mcp-usage-routes.js.map +0 -1
  172. package/dist/shared/mcp/tenant-context.d.ts +0 -59
  173. package/dist/shared/mcp/tenant-context.d.ts.map +0 -1
  174. package/dist/shared/mcp/tenant-context.js +0 -136
  175. package/dist/shared/mcp/tenant-context.js.map +0 -1
  176. package/dist/shared/mcp/types.d.ts +0 -74
  177. package/dist/shared/mcp/types.d.ts.map +0 -1
  178. package/dist/shared/mcp/types.js +0 -7
  179. package/dist/shared/mcp/types.js.map +0 -1
  180. package/dist/shared/planner/GoogleCalendarService.d.ts +0 -137
  181. package/dist/shared/planner/GoogleCalendarService.d.ts.map +0 -1
  182. package/dist/shared/planner/GoogleCalendarService.js +0 -525
  183. package/dist/shared/planner/GoogleCalendarService.js.map +0 -1
  184. package/dist/shared/planner/PlannerService.d.ts +0 -264
  185. package/dist/shared/planner/PlannerService.d.ts.map +0 -1
  186. package/dist/shared/planner/PlannerService.js +0 -1393
  187. package/dist/shared/planner/PlannerService.js.map +0 -1
  188. package/dist/shared/planner/index.d.ts +0 -37
  189. package/dist/shared/planner/index.d.ts.map +0 -1
  190. package/dist/shared/planner/index.js +0 -35
  191. package/dist/shared/planner/index.js.map +0 -1
  192. package/dist/shared/planner/intervals.d.ts +0 -60
  193. package/dist/shared/planner/intervals.d.ts.map +0 -1
  194. package/dist/shared/planner/intervals.js +0 -141
  195. package/dist/shared/planner/intervals.js.map +0 -1
  196. package/dist/shared/planner/routes.d.ts +0 -69
  197. package/dist/shared/planner/routes.d.ts.map +0 -1
  198. package/dist/shared/planner/routes.js +0 -770
  199. package/dist/shared/planner/routes.js.map +0 -1
  200. package/dist/shared/planner/types.d.ts +0 -328
  201. package/dist/shared/planner/types.d.ts.map +0 -1
  202. package/dist/shared/planner/types.js +0 -9
  203. package/dist/shared/planner/types.js.map +0 -1
  204. package/dist/shared/storage/ImageProcessingService.d.ts +0 -32
  205. package/dist/shared/storage/ImageProcessingService.d.ts.map +0 -1
  206. package/dist/shared/storage/ImageProcessingService.js +0 -127
  207. package/dist/shared/storage/ImageProcessingService.js.map +0 -1
  208. package/dist/shared/storage/StorageProxyService.d.ts +0 -47
  209. package/dist/shared/storage/StorageProxyService.d.ts.map +0 -1
  210. package/dist/shared/storage/StorageProxyService.js +0 -196
  211. package/dist/shared/storage/StorageProxyService.js.map +0 -1
  212. package/dist/shared/storage/StorageUploadService.d.ts +0 -126
  213. package/dist/shared/storage/StorageUploadService.d.ts.map +0 -1
  214. package/dist/shared/storage/StorageUploadService.js +0 -206
  215. package/dist/shared/storage/StorageUploadService.js.map +0 -1
  216. package/dist/shared/storage/creative-urls.d.ts +0 -14
  217. package/dist/shared/storage/creative-urls.d.ts.map +0 -1
  218. package/dist/shared/storage/creative-urls.js +0 -30
  219. package/dist/shared/storage/creative-urls.js.map +0 -1
  220. package/dist/shared/storage/index.d.ts +0 -28
  221. package/dist/shared/storage/index.d.ts.map +0 -1
  222. package/dist/shared/storage/index.js +0 -27
  223. package/dist/shared/storage/index.js.map +0 -1
  224. package/dist/shared/storage/routes.d.ts +0 -42
  225. package/dist/shared/storage/routes.d.ts.map +0 -1
  226. package/dist/shared/storage/routes.js +0 -160
  227. package/dist/shared/storage/routes.js.map +0 -1
  228. package/dist/shared/storage/types.d.ts +0 -53
  229. package/dist/shared/storage/types.d.ts.map +0 -1
  230. package/dist/shared/storage/types.js +0 -2
  231. package/dist/shared/storage/types.js.map +0 -1
  232. package/dist/shared/telegram/index.d.ts +0 -4
  233. package/dist/shared/telegram/index.d.ts.map +0 -1
  234. package/dist/shared/telegram/index.js +0 -3
  235. package/dist/shared/telegram/index.js.map +0 -1
  236. package/dist/shared/telegram/routes.d.ts +0 -43
  237. package/dist/shared/telegram/routes.d.ts.map +0 -1
  238. package/dist/shared/telegram/routes.js +0 -868
  239. package/dist/shared/telegram/routes.js.map +0 -1
  240. package/dist/shared/telegram/types.d.ts +0 -168
  241. package/dist/shared/telegram/types.d.ts.map +0 -1
  242. package/dist/shared/telegram/types.js +0 -7
  243. package/dist/shared/telegram/types.js.map +0 -1
  244. package/dist/shared/telegram/utils.d.ts +0 -44
  245. package/dist/shared/telegram/utils.d.ts.map +0 -1
  246. package/dist/shared/telegram/utils.js +0 -121
  247. package/dist/shared/telegram/utils.js.map +0 -1
  248. package/dist/storage.d.ts +0 -9
  249. package/dist/storage.d.ts.map +0 -1
  250. package/dist/storage.js +0 -8
  251. package/dist/storage.js.map +0 -1
  252. package/dist/telemetry.d.ts +0 -9
  253. package/dist/telemetry.d.ts.map +0 -1
  254. package/dist/telemetry.js +0 -8
  255. package/dist/telemetry.js.map +0 -1
  256. package/scripts/postinstall.js +0 -79
  257. package/src/shared/affiliate/migrations/001_create_affiliates.sql +0 -49
  258. package/src/shared/affiliate/migrations/002_create_affiliate_commissions.sql +0 -31
  259. package/src/shared/affiliate/migrations/003_create_affiliate_clicks.sql +0 -26
  260. package/src/shared/affiliate/migrations/004_create_affiliate_payments.sql +0 -34
  261. package/src/shared/affiliate/migrations/005_create_affiliate_tier_history.sql +0 -19
  262. package/src/shared/affiliate/migrations/006_create_affiliate_rpc_functions.sql +0 -209
  263. package/src/shared/affiliate/migrations/007_create_affiliate_rls_policies.sql +0 -123
  264. package/src/shared/billing/migrations/00000000000001_billing.sql +0 -114
  265. package/src/shared/email/migrations/000_create_email_logs.sql +0 -27
  266. package/src/shared/email/migrations/001_create_email_templates.sql +0 -27
  267. package/src/shared/email/migrations/002_add_rls_baseline_policies.sql +0 -37
  268. package/src/shared/email/migrations/003_create_gmail_accounts.sql +0 -82
  269. package/src/shared/email/migrations/004_add_email_logs_tracking_columns.sql +0 -15
  270. package/src/shared/mcp/migrations/001_mcp_api_tokens.sql +0 -21
  271. 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