lsh-framework 3.2.4 → 3.5.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/LICENSE +21 -0
- package/README.md +72 -34
- package/dist/commands/ipfs.js +7 -12
- package/dist/commands/sync.js +51 -39
- package/dist/constants/config.js +3 -0
- package/dist/lib/floating-point-arithmetic.js +2 -2
- package/dist/lib/ipfs-client-manager.js +51 -13
- package/dist/lib/ipfs-secrets-storage.js +21 -16
- package/dist/lib/ipfs-sync.js +88 -14
- package/dist/lib/secrets-manager.js +117 -47
- package/dist/lib/sync-key-store.js +87 -0
- package/dist/services/secrets/secrets.js +77 -39
- package/package.json +16 -16
- package/dist/__tests__/fixtures/job-fixtures.js +0 -204
- package/dist/__tests__/fixtures/supabase-mocks.js +0 -252
- package/dist/daemon/job-registry.js +0 -556
- package/dist/daemon/lshd.js +0 -968
- package/dist/daemon/saas-api-routes.js +0 -599
- package/dist/daemon/saas-api-server.js +0 -231
- package/dist/examples/supabase-integration.js +0 -106
- package/dist/lib/api-response.js +0 -226
- package/dist/lib/base-command-registrar.js +0 -287
- package/dist/lib/base-job-manager.js +0 -295
- package/dist/lib/cloud-config-manager.js +0 -348
- package/dist/lib/cron-job-manager.js +0 -368
- package/dist/lib/daemon-client-helper.js +0 -145
- package/dist/lib/daemon-client.js +0 -513
- package/dist/lib/database-persistence.js +0 -727
- package/dist/lib/database-schema.js +0 -259
- package/dist/lib/database-types.js +0 -90
- package/dist/lib/enhanced-history-system.js +0 -247
- package/dist/lib/history-system.js +0 -246
- package/dist/lib/job-manager.js +0 -436
- package/dist/lib/job-storage-database.js +0 -164
- package/dist/lib/job-storage-memory.js +0 -73
- package/dist/lib/local-storage-adapter.js +0 -507
- package/dist/lib/optimized-job-scheduler.js +0 -356
- package/dist/lib/saas-audit.js +0 -215
- package/dist/lib/saas-auth.js +0 -465
- package/dist/lib/saas-billing.js +0 -503
- package/dist/lib/saas-email.js +0 -403
- package/dist/lib/saas-encryption.js +0 -221
- package/dist/lib/saas-organizations.js +0 -662
- package/dist/lib/saas-secrets.js +0 -408
- package/dist/lib/saas-types.js +0 -165
- package/dist/lib/supabase-client.js +0 -125
- package/dist/lib/supabase-utils.js +0 -396
- package/dist/services/cron/cron-registrar.js +0 -240
- package/dist/services/cron/cron.js +0 -9
- package/dist/services/daemon/daemon-registrar.js +0 -585
- package/dist/services/daemon/daemon.js +0 -9
- package/dist/services/supabase/supabase-registrar.js +0 -375
- package/dist/services/supabase/supabase.js +0 -9
package/dist/lib/saas-billing.js
DELETED
|
@@ -1,503 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LSH SaaS Billing Service
|
|
3
|
-
* Stripe integration for subscriptions and billing
|
|
4
|
-
*/
|
|
5
|
-
import { getSupabaseClient } from './supabase-client.js';
|
|
6
|
-
import { auditLogger } from './saas-audit.js';
|
|
7
|
-
import { ENV_VARS } from '../constants/index.js';
|
|
8
|
-
/**
|
|
9
|
-
* Stripe Pricing IDs (set via environment variables)
|
|
10
|
-
*/
|
|
11
|
-
export const STRIPE_PRICE_IDS = {
|
|
12
|
-
pro_monthly: process.env[ENV_VARS.STRIPE_PRICE_PRO_MONTHLY] || '',
|
|
13
|
-
pro_yearly: process.env[ENV_VARS.STRIPE_PRICE_PRO_YEARLY] || '',
|
|
14
|
-
enterprise_monthly: process.env[ENV_VARS.STRIPE_PRICE_ENTERPRISE_MONTHLY] || '',
|
|
15
|
-
enterprise_yearly: process.env[ENV_VARS.STRIPE_PRICE_ENTERPRISE_YEARLY] || '',
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Billing Service
|
|
19
|
-
*/
|
|
20
|
-
export class BillingService {
|
|
21
|
-
supabase = getSupabaseClient();
|
|
22
|
-
stripeSecretKey = process.env[ENV_VARS.STRIPE_SECRET_KEY] || '';
|
|
23
|
-
stripeWebhookSecret = process.env[ENV_VARS.STRIPE_WEBHOOK_SECRET] || '';
|
|
24
|
-
stripeApiUrl = 'https://api.stripe.com/v1';
|
|
25
|
-
/**
|
|
26
|
-
* Create Stripe customer
|
|
27
|
-
*/
|
|
28
|
-
async createStripeCustomer(params) {
|
|
29
|
-
if (!this.stripeSecretKey) {
|
|
30
|
-
throw new Error('STRIPE_SECRET_KEY not configured');
|
|
31
|
-
}
|
|
32
|
-
const formData = new URLSearchParams();
|
|
33
|
-
formData.append('email', params.email);
|
|
34
|
-
if (params.name) {
|
|
35
|
-
formData.append('name', params.name);
|
|
36
|
-
}
|
|
37
|
-
formData.append('metadata[organization_id]', params.organizationId);
|
|
38
|
-
const response = await fetch(`${this.stripeApiUrl}/customers`, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: {
|
|
41
|
-
Authorization: `Bearer ${this.stripeSecretKey}`,
|
|
42
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
43
|
-
},
|
|
44
|
-
body: formData,
|
|
45
|
-
});
|
|
46
|
-
if (!response.ok) {
|
|
47
|
-
const error = await response.text();
|
|
48
|
-
throw new Error(`Failed to create Stripe customer: ${error}`);
|
|
49
|
-
}
|
|
50
|
-
const customer = await response.json();
|
|
51
|
-
// Update organization with Stripe customer ID
|
|
52
|
-
await this.supabase
|
|
53
|
-
.from('organizations')
|
|
54
|
-
.update({ stripe_customer_id: customer.id })
|
|
55
|
-
.eq('id', params.organizationId);
|
|
56
|
-
return customer.id;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Create checkout session
|
|
60
|
-
*/
|
|
61
|
-
async createCheckoutSession(params) {
|
|
62
|
-
if (!this.stripeSecretKey) {
|
|
63
|
-
throw new Error('STRIPE_SECRET_KEY not configured');
|
|
64
|
-
}
|
|
65
|
-
// Get price ID
|
|
66
|
-
const priceKey = `${params.tier}_${params.billingPeriod}`;
|
|
67
|
-
const priceId = STRIPE_PRICE_IDS[priceKey];
|
|
68
|
-
if (!priceId) {
|
|
69
|
-
throw new Error(`No Stripe price configured for ${params.tier} ${params.billingPeriod}`);
|
|
70
|
-
}
|
|
71
|
-
const formData = new URLSearchParams();
|
|
72
|
-
formData.append('mode', 'subscription');
|
|
73
|
-
formData.append('line_items[0][price]', priceId);
|
|
74
|
-
formData.append('line_items[0][quantity]', '1');
|
|
75
|
-
formData.append('success_url', params.successUrl);
|
|
76
|
-
formData.append('cancel_url', params.cancelUrl);
|
|
77
|
-
formData.append('metadata[organization_id]', params.organizationId);
|
|
78
|
-
formData.append('subscription_data[metadata][organization_id]', params.organizationId);
|
|
79
|
-
if (params.customerId) {
|
|
80
|
-
formData.append('customer', params.customerId);
|
|
81
|
-
}
|
|
82
|
-
const response = await fetch(`${this.stripeApiUrl}/checkout/sessions`, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: {
|
|
85
|
-
Authorization: `Bearer ${this.stripeSecretKey}`,
|
|
86
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
87
|
-
},
|
|
88
|
-
body: formData,
|
|
89
|
-
});
|
|
90
|
-
if (!response.ok) {
|
|
91
|
-
const error = await response.text();
|
|
92
|
-
throw new Error(`Failed to create checkout session: ${error}`);
|
|
93
|
-
}
|
|
94
|
-
const session = await response.json();
|
|
95
|
-
return {
|
|
96
|
-
sessionId: session.id,
|
|
97
|
-
url: session.url,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Create billing portal session
|
|
102
|
-
*/
|
|
103
|
-
async createPortalSession(customerId, returnUrl) {
|
|
104
|
-
if (!this.stripeSecretKey) {
|
|
105
|
-
throw new Error('STRIPE_SECRET_KEY not configured');
|
|
106
|
-
}
|
|
107
|
-
const formData = new URLSearchParams();
|
|
108
|
-
formData.append('customer', customerId);
|
|
109
|
-
formData.append('return_url', returnUrl);
|
|
110
|
-
const response = await fetch(`${this.stripeApiUrl}/billing_portal/sessions`, {
|
|
111
|
-
method: 'POST',
|
|
112
|
-
headers: {
|
|
113
|
-
Authorization: `Bearer ${this.stripeSecretKey}`,
|
|
114
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
115
|
-
},
|
|
116
|
-
body: formData,
|
|
117
|
-
});
|
|
118
|
-
if (!response.ok) {
|
|
119
|
-
const error = await response.text();
|
|
120
|
-
throw new Error(`Failed to create portal session: ${error}`);
|
|
121
|
-
}
|
|
122
|
-
const session = await response.json();
|
|
123
|
-
return session.url;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Handle Stripe webhook
|
|
127
|
-
*/
|
|
128
|
-
async handleWebhook(payload, signature) {
|
|
129
|
-
if (!this.stripeWebhookSecret) {
|
|
130
|
-
throw new Error('STRIPE_WEBHOOK_SECRET not configured');
|
|
131
|
-
}
|
|
132
|
-
// Verify webhook signature
|
|
133
|
-
const event = this.verifyWebhookSignature(payload, signature);
|
|
134
|
-
// Handle different event types
|
|
135
|
-
switch (event.type) {
|
|
136
|
-
case 'checkout.session.completed':
|
|
137
|
-
await this.handleCheckoutCompleted(event.data.object);
|
|
138
|
-
break;
|
|
139
|
-
case 'customer.subscription.created':
|
|
140
|
-
case 'customer.subscription.updated':
|
|
141
|
-
await this.handleSubscriptionUpdated(event.data.object);
|
|
142
|
-
break;
|
|
143
|
-
case 'customer.subscription.deleted':
|
|
144
|
-
await this.handleSubscriptionDeleted(event.data.object);
|
|
145
|
-
break;
|
|
146
|
-
case 'invoice.paid':
|
|
147
|
-
await this.handleInvoicePaid(event.data.object);
|
|
148
|
-
break;
|
|
149
|
-
case 'invoice.payment_failed':
|
|
150
|
-
await this.handleInvoicePaymentFailed(event.data.object);
|
|
151
|
-
break;
|
|
152
|
-
default:
|
|
153
|
-
console.log(`Unhandled webhook event: ${event.type}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Verify Stripe webhook signature and parse event payload.
|
|
158
|
-
*
|
|
159
|
-
* In production, this should use Stripe's signature verification:
|
|
160
|
-
* ```typescript
|
|
161
|
-
* const event = stripe.webhooks.constructEvent(
|
|
162
|
-
* payload, signature, this.stripeWebhookSecret
|
|
163
|
-
* );
|
|
164
|
-
* ```
|
|
165
|
-
*
|
|
166
|
-
* Current implementation parses JSON without verification (TODO: implement proper verification).
|
|
167
|
-
*
|
|
168
|
-
* @param payload - Raw webhook body as string
|
|
169
|
-
* @param _signature - Stripe-Signature header value (not yet used)
|
|
170
|
-
* @returns Parsed Stripe event object
|
|
171
|
-
* @throws Error if payload is not valid JSON
|
|
172
|
-
* @see https://stripe.com/docs/webhooks/signatures
|
|
173
|
-
*/
|
|
174
|
-
verifyWebhookSignature(payload, _signature) {
|
|
175
|
-
// In production, use Stripe's webhook signature verification
|
|
176
|
-
// For now, just parse the payload
|
|
177
|
-
try {
|
|
178
|
-
return JSON.parse(payload);
|
|
179
|
-
}
|
|
180
|
-
catch (_error) {
|
|
181
|
-
throw new Error('Invalid webhook payload');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Handle Stripe checkout.session.completed webhook event.
|
|
186
|
-
*
|
|
187
|
-
* Called when a customer completes checkout. The actual subscription
|
|
188
|
-
* creation is handled by the customer.subscription.created event.
|
|
189
|
-
*
|
|
190
|
-
* Extracts organization_id from session.metadata to link the checkout
|
|
191
|
-
* to the correct organization.
|
|
192
|
-
*
|
|
193
|
-
* @param session - Stripe checkout session object
|
|
194
|
-
* @see StripeCheckoutSession in database-types.ts for partial type
|
|
195
|
-
*/
|
|
196
|
-
async handleCheckoutCompleted(session) {
|
|
197
|
-
const organizationId = session.metadata?.organization_id;
|
|
198
|
-
if (!organizationId) {
|
|
199
|
-
console.error('No organization_id in checkout session metadata');
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
// Subscription will be created via customer.subscription.created event
|
|
203
|
-
console.log(`Checkout completed for organization ${organizationId}`);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Handle Stripe customer.subscription.created/updated webhook events.
|
|
207
|
-
*
|
|
208
|
-
* Creates or updates subscription record in database and syncs tier
|
|
209
|
-
* to the organization. Key operations:
|
|
210
|
-
* 1. Extracts organization_id from subscription.metadata
|
|
211
|
-
* 2. Determines tier from price ID (maps Stripe price → 'free' | 'pro' | 'enterprise')
|
|
212
|
-
* 3. Upserts subscription record with all billing details
|
|
213
|
-
* 4. Updates organization's subscription_tier and subscription_status
|
|
214
|
-
* 5. Logs audit event
|
|
215
|
-
*
|
|
216
|
-
* Timestamps from Stripe are Unix timestamps (seconds), converted to ISO strings.
|
|
217
|
-
*
|
|
218
|
-
* @param subscription - Stripe subscription object
|
|
219
|
-
* @see StripeSubscriptionEvent in database-types.ts for partial type
|
|
220
|
-
*/
|
|
221
|
-
async handleSubscriptionUpdated(subscription) {
|
|
222
|
-
const organizationId = subscription.metadata?.organization_id;
|
|
223
|
-
if (!organizationId) {
|
|
224
|
-
console.error('No organization_id in subscription metadata');
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
// Determine tier from price ID
|
|
228
|
-
const priceId = subscription.items?.data[0]?.price?.id;
|
|
229
|
-
const tier = this.getTierFromPriceId(priceId);
|
|
230
|
-
// Upsert subscription
|
|
231
|
-
const { error } = await this.supabase
|
|
232
|
-
.from('subscriptions')
|
|
233
|
-
.upsert({
|
|
234
|
-
organization_id: organizationId,
|
|
235
|
-
stripe_subscription_id: subscription.id,
|
|
236
|
-
stripe_price_id: priceId,
|
|
237
|
-
stripe_product_id: subscription.items?.data[0]?.price?.product,
|
|
238
|
-
tier,
|
|
239
|
-
status: subscription.status,
|
|
240
|
-
current_period_start: new Date(subscription.current_period_start * 1000).toISOString(),
|
|
241
|
-
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
|
|
242
|
-
cancel_at_period_end: subscription.cancel_at_period_end,
|
|
243
|
-
trial_start: subscription.trial_start
|
|
244
|
-
? new Date(subscription.trial_start * 1000).toISOString()
|
|
245
|
-
: null,
|
|
246
|
-
trial_end: subscription.trial_end
|
|
247
|
-
? new Date(subscription.trial_end * 1000).toISOString()
|
|
248
|
-
: null,
|
|
249
|
-
}, { onConflict: 'stripe_subscription_id' });
|
|
250
|
-
if (error) {
|
|
251
|
-
console.error('Failed to upsert subscription:', error);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
// Update organization tier
|
|
255
|
-
await this.supabase
|
|
256
|
-
.from('organizations')
|
|
257
|
-
.update({
|
|
258
|
-
subscription_tier: tier,
|
|
259
|
-
subscription_status: subscription.status,
|
|
260
|
-
subscription_expires_at: new Date(subscription.current_period_end * 1000).toISOString(),
|
|
261
|
-
})
|
|
262
|
-
.eq('id', organizationId);
|
|
263
|
-
await auditLogger.log({
|
|
264
|
-
organizationId,
|
|
265
|
-
action: 'billing.subscription_updated',
|
|
266
|
-
resourceType: 'subscription',
|
|
267
|
-
resourceId: subscription.id,
|
|
268
|
-
newValue: { tier, status: subscription.status },
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Handle Stripe customer.subscription.deleted webhook event.
|
|
273
|
-
*
|
|
274
|
-
* Called when a subscription is canceled (immediate or at period end).
|
|
275
|
-
* Operations:
|
|
276
|
-
* 1. Marks subscription as 'canceled' with canceled_at timestamp
|
|
277
|
-
* 2. Downgrades organization to 'free' tier
|
|
278
|
-
* 3. Updates organization subscription_status to 'canceled'
|
|
279
|
-
* 4. Logs audit event
|
|
280
|
-
*
|
|
281
|
-
* @param subscription - Stripe subscription object
|
|
282
|
-
* @see StripeSubscriptionEvent in database-types.ts for partial type
|
|
283
|
-
*/
|
|
284
|
-
async handleSubscriptionDeleted(subscription) {
|
|
285
|
-
const organizationId = subscription.metadata?.organization_id;
|
|
286
|
-
if (!organizationId) {
|
|
287
|
-
console.error('No organization_id in subscription metadata');
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
// Mark subscription as canceled
|
|
291
|
-
await this.supabase
|
|
292
|
-
.from('subscriptions')
|
|
293
|
-
.update({
|
|
294
|
-
status: 'canceled',
|
|
295
|
-
canceled_at: new Date().toISOString(),
|
|
296
|
-
})
|
|
297
|
-
.eq('stripe_subscription_id', subscription.id);
|
|
298
|
-
// Downgrade organization to free tier
|
|
299
|
-
await this.supabase
|
|
300
|
-
.from('organizations')
|
|
301
|
-
.update({
|
|
302
|
-
subscription_tier: 'free',
|
|
303
|
-
subscription_status: 'canceled',
|
|
304
|
-
})
|
|
305
|
-
.eq('id', organizationId);
|
|
306
|
-
await auditLogger.log({
|
|
307
|
-
organizationId,
|
|
308
|
-
action: 'billing.subscription_canceled',
|
|
309
|
-
resourceType: 'subscription',
|
|
310
|
-
resourceId: subscription.id,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Handle Stripe invoice.paid webhook event.
|
|
315
|
-
*
|
|
316
|
-
* Records successful payment in the invoices table. Extracts organization_id
|
|
317
|
-
* from invoice.subscription_metadata (set during checkout).
|
|
318
|
-
*
|
|
319
|
-
* Amounts are in cents (e.g., 1000 = $10.00). Currency is uppercased.
|
|
320
|
-
*
|
|
321
|
-
* @param invoice - Stripe invoice object
|
|
322
|
-
* @see StripeInvoiceEvent in database-types.ts for partial type
|
|
323
|
-
*/
|
|
324
|
-
async handleInvoicePaid(invoice) {
|
|
325
|
-
const organizationId = invoice.subscription_metadata?.organization_id;
|
|
326
|
-
if (!organizationId) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
// Record invoice
|
|
330
|
-
await this.supabase.from('invoices').upsert({
|
|
331
|
-
organization_id: organizationId,
|
|
332
|
-
stripe_invoice_id: invoice.id,
|
|
333
|
-
number: invoice.number,
|
|
334
|
-
amount_due: invoice.amount_due,
|
|
335
|
-
amount_paid: invoice.amount_paid,
|
|
336
|
-
currency: invoice.currency?.toUpperCase() || 'USD',
|
|
337
|
-
status: 'paid',
|
|
338
|
-
invoice_date: new Date(invoice.created * 1000).toISOString(),
|
|
339
|
-
paid_at: invoice.status_transitions?.paid_at ? new Date(invoice.status_transitions.paid_at * 1000).toISOString() : null,
|
|
340
|
-
invoice_pdf_url: invoice.invoice_pdf,
|
|
341
|
-
}, { onConflict: 'stripe_invoice_id' });
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Handle Stripe invoice.payment_failed webhook event.
|
|
345
|
-
*
|
|
346
|
-
* Called when payment fails (declined card, insufficient funds, etc.).
|
|
347
|
-
* Updates organization subscription_status to 'past_due' and logs audit event.
|
|
348
|
-
*
|
|
349
|
-
* Note: Does not immediately downgrade tier. Stripe will retry payment
|
|
350
|
-
* according to your dunning settings. Downgrade happens on subscription.deleted.
|
|
351
|
-
*
|
|
352
|
-
* @param invoice - Stripe invoice object
|
|
353
|
-
* @see StripeInvoiceEvent in database-types.ts for partial type
|
|
354
|
-
*/
|
|
355
|
-
async handleInvoicePaymentFailed(invoice) {
|
|
356
|
-
const organizationId = invoice.subscription_metadata?.organization_id;
|
|
357
|
-
if (!organizationId) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
// Update organization status
|
|
361
|
-
await this.supabase
|
|
362
|
-
.from('organizations')
|
|
363
|
-
.update({
|
|
364
|
-
subscription_status: 'past_due',
|
|
365
|
-
})
|
|
366
|
-
.eq('id', organizationId);
|
|
367
|
-
await auditLogger.log({
|
|
368
|
-
organizationId,
|
|
369
|
-
action: 'billing.payment_failed',
|
|
370
|
-
resourceType: 'subscription',
|
|
371
|
-
metadata: { invoice_id: invoice.id },
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Get tier from Stripe price ID
|
|
376
|
-
*/
|
|
377
|
-
getTierFromPriceId(priceId) {
|
|
378
|
-
if (!priceId)
|
|
379
|
-
return 'free';
|
|
380
|
-
if (priceId === STRIPE_PRICE_IDS.pro_monthly ||
|
|
381
|
-
priceId === STRIPE_PRICE_IDS.pro_yearly) {
|
|
382
|
-
return 'pro';
|
|
383
|
-
}
|
|
384
|
-
if (priceId === STRIPE_PRICE_IDS.enterprise_monthly ||
|
|
385
|
-
priceId === STRIPE_PRICE_IDS.enterprise_yearly) {
|
|
386
|
-
return 'enterprise';
|
|
387
|
-
}
|
|
388
|
-
return 'free';
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Get subscription for organization
|
|
392
|
-
*/
|
|
393
|
-
async getOrganizationSubscription(organizationId) {
|
|
394
|
-
const { data, error } = await this.supabase
|
|
395
|
-
.from('subscriptions')
|
|
396
|
-
.select('*')
|
|
397
|
-
.eq('organization_id', organizationId)
|
|
398
|
-
.order('created_at', { ascending: false })
|
|
399
|
-
.limit(1)
|
|
400
|
-
.single();
|
|
401
|
-
if (error || !data) {
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
return this.mapDbSubscriptionToSubscription(data);
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Get invoices for organization
|
|
408
|
-
*/
|
|
409
|
-
async getOrganizationInvoices(organizationId) {
|
|
410
|
-
const { data, error } = await this.supabase
|
|
411
|
-
.from('invoices')
|
|
412
|
-
.select('*')
|
|
413
|
-
.eq('organization_id', organizationId)
|
|
414
|
-
.order('invoice_date', { ascending: false });
|
|
415
|
-
if (error) {
|
|
416
|
-
throw new Error(`Failed to get invoices: ${error.message}`);
|
|
417
|
-
}
|
|
418
|
-
return (data || []).map(this.mapDbInvoiceToInvoice);
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Transform Supabase subscription record to domain model.
|
|
422
|
-
*
|
|
423
|
-
* Maps database snake_case columns to TypeScript camelCase properties:
|
|
424
|
-
* - `organization_id` → `organizationId`
|
|
425
|
-
* - `stripe_subscription_id` → `stripeSubscriptionId` (Stripe sub_xxx ID)
|
|
426
|
-
* - `stripe_price_id` → `stripePriceId` (Stripe price_xxx ID)
|
|
427
|
-
* - `stripe_product_id` → `stripeProductId` (Stripe prod_xxx ID)
|
|
428
|
-
* - `tier` → `tier` (SubscriptionTier: 'free' | 'pro' | 'enterprise')
|
|
429
|
-
* - `status` → `status` (SubscriptionStatus)
|
|
430
|
-
* - `current_period_start` → `currentPeriodStart` (nullable Date)
|
|
431
|
-
* - `current_period_end` → `currentPeriodEnd` (nullable Date)
|
|
432
|
-
* - `cancel_at_period_end` → `cancelAtPeriodEnd` (boolean)
|
|
433
|
-
* - `trial_start` → `trialStart` (nullable Date)
|
|
434
|
-
* - `trial_end` → `trialEnd` (nullable Date)
|
|
435
|
-
* - `canceled_at` → `canceledAt` (nullable Date)
|
|
436
|
-
*
|
|
437
|
-
* @param dbSub - Supabase record from 'subscriptions' table
|
|
438
|
-
* @returns Domain Subscription object
|
|
439
|
-
* @see DbSubscriptionRecord in database-types.ts for input shape
|
|
440
|
-
* @see Subscription in saas-types.ts for output shape
|
|
441
|
-
*/
|
|
442
|
-
mapDbSubscriptionToSubscription(dbSub) {
|
|
443
|
-
return {
|
|
444
|
-
id: dbSub.id,
|
|
445
|
-
organizationId: dbSub.organization_id,
|
|
446
|
-
stripeSubscriptionId: dbSub.stripe_subscription_id,
|
|
447
|
-
stripePriceId: dbSub.stripe_price_id,
|
|
448
|
-
stripeProductId: dbSub.stripe_product_id,
|
|
449
|
-
tier: dbSub.tier,
|
|
450
|
-
status: dbSub.status,
|
|
451
|
-
currentPeriodStart: dbSub.current_period_start
|
|
452
|
-
? new Date(dbSub.current_period_start)
|
|
453
|
-
: null,
|
|
454
|
-
currentPeriodEnd: dbSub.current_period_end ? new Date(dbSub.current_period_end) : null,
|
|
455
|
-
cancelAtPeriodEnd: dbSub.cancel_at_period_end,
|
|
456
|
-
trialStart: dbSub.trial_start ? new Date(dbSub.trial_start) : null,
|
|
457
|
-
trialEnd: dbSub.trial_end ? new Date(dbSub.trial_end) : null,
|
|
458
|
-
createdAt: new Date(dbSub.created_at),
|
|
459
|
-
updatedAt: new Date(dbSub.updated_at),
|
|
460
|
-
canceledAt: dbSub.canceled_at ? new Date(dbSub.canceled_at) : null,
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Transform Supabase invoice record to domain model.
|
|
465
|
-
*
|
|
466
|
-
* Maps database snake_case columns to TypeScript camelCase properties:
|
|
467
|
-
* - `organization_id` → `organizationId`
|
|
468
|
-
* - `stripe_invoice_id` → `stripeInvoiceId` (Stripe in_xxx ID)
|
|
469
|
-
* - `amount_due` → `amountDue` (in cents, e.g., 1000 = $10.00)
|
|
470
|
-
* - `amount_paid` → `amountPaid` (in cents)
|
|
471
|
-
* - `invoice_date` → `invoiceDate` (Date)
|
|
472
|
-
* - `due_date` → `dueDate` (nullable Date)
|
|
473
|
-
* - `paid_at` → `paidAt` (nullable Date)
|
|
474
|
-
* - `invoice_pdf_url` → `invoicePdfUrl` (Stripe-hosted PDF URL)
|
|
475
|
-
*
|
|
476
|
-
* @param dbInvoice - Supabase record from 'invoices' table
|
|
477
|
-
* @returns Domain Invoice object
|
|
478
|
-
* @see DbInvoiceRecord in database-types.ts for input shape
|
|
479
|
-
* @see Invoice in saas-types.ts for output shape
|
|
480
|
-
*/
|
|
481
|
-
mapDbInvoiceToInvoice(dbInvoice) {
|
|
482
|
-
return {
|
|
483
|
-
id: dbInvoice.id,
|
|
484
|
-
organizationId: dbInvoice.organization_id,
|
|
485
|
-
stripeInvoiceId: dbInvoice.stripe_invoice_id,
|
|
486
|
-
number: dbInvoice.number,
|
|
487
|
-
amountDue: dbInvoice.amount_due,
|
|
488
|
-
amountPaid: dbInvoice.amount_paid,
|
|
489
|
-
currency: dbInvoice.currency,
|
|
490
|
-
status: dbInvoice.status,
|
|
491
|
-
invoiceDate: new Date(dbInvoice.invoice_date),
|
|
492
|
-
dueDate: dbInvoice.due_date ? new Date(dbInvoice.due_date) : null,
|
|
493
|
-
paidAt: dbInvoice.paid_at ? new Date(dbInvoice.paid_at) : null,
|
|
494
|
-
invoicePdfUrl: dbInvoice.invoice_pdf_url,
|
|
495
|
-
createdAt: new Date(dbInvoice.created_at),
|
|
496
|
-
updatedAt: new Date(dbInvoice.updated_at),
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Singleton instance
|
|
502
|
-
*/
|
|
503
|
-
export const billingService = new BillingService();
|