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