Initial commit — eLegal Software monorepo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getDb, firms } from '@lawdesk/db';
|
||||
import { env } from '../env';
|
||||
import { getStripe, getPlanConfig, stripeIsConfigured } from '../lib/stripe';
|
||||
|
||||
export async function billingRoutes(app: FastifyInstance) {
|
||||
app.addHook('preHandler', app.requireFirm);
|
||||
|
||||
// Status — what does the UI need to show? Configured at all? Current plan? Has subscription?
|
||||
app.get('/api/billing/status', async (req) => {
|
||||
const firmId = req.user!.firmId!;
|
||||
const [firm] = await getDb().select().from(firms).where(eq(firms.id, firmId)).limit(1);
|
||||
return {
|
||||
configured: stripeIsConfigured(),
|
||||
plan: firm?.plan ?? 'starter',
|
||||
hasSubscription: !!firm?.stripeSubscriptionId,
|
||||
hasCustomer: !!firm?.stripeCustomerId,
|
||||
};
|
||||
});
|
||||
|
||||
// Create a Checkout Session — returns the URL to redirect the user to.
|
||||
app.post('/api/billing/checkout', async (req, reply) => {
|
||||
const parsed = z
|
||||
.object({ plan: z.enum(['pro', 'lifetime']) })
|
||||
.safeParse(req.body);
|
||||
if (!parsed.success) return reply.code(400).send({ error: 'invalid_plan' });
|
||||
|
||||
if (!stripeIsConfigured()) return reply.code(503).send({ error: 'stripe_not_configured' });
|
||||
|
||||
const firmId = req.user!.firmId!;
|
||||
const userEmail = req.user!.email;
|
||||
const planCfg = getPlanConfig(parsed.data.plan);
|
||||
if (!planCfg) return reply.code(503).send({ error: 'plan_not_configured' });
|
||||
|
||||
const db = getDb();
|
||||
const [firm] = await db.select().from(firms).where(eq(firms.id, firmId)).limit(1);
|
||||
if (!firm) return reply.code(404).send({ error: 'firm_not_found' });
|
||||
|
||||
const stripe = getStripe();
|
||||
|
||||
// Reuse the customer if we've made one before; otherwise let Checkout create one and we'll
|
||||
// capture it on the webhook.
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: planCfg.mode,
|
||||
line_items: [{ price: planCfg.priceId, quantity: 1 }],
|
||||
customer: firm.stripeCustomerId ?? undefined,
|
||||
customer_email: firm.stripeCustomerId ? undefined : userEmail,
|
||||
client_reference_id: firmId,
|
||||
metadata: { firmId, plan: planCfg.planName },
|
||||
subscription_data:
|
||||
planCfg.mode === 'subscription' ? { metadata: { firmId, plan: planCfg.planName } } : undefined,
|
||||
success_url: `${env.PUBLIC_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${env.PUBLIC_URL}/billing/cancel`,
|
||||
allow_promotion_codes: true,
|
||||
});
|
||||
|
||||
return { url: session.url };
|
||||
});
|
||||
|
||||
// Customer Portal — for managing the subscription, updating payment method, viewing invoices.
|
||||
app.post('/api/billing/portal', async (req, reply) => {
|
||||
if (!stripeIsConfigured()) return reply.code(503).send({ error: 'stripe_not_configured' });
|
||||
|
||||
const firmId = req.user!.firmId!;
|
||||
const [firm] = await getDb().select().from(firms).where(eq(firms.id, firmId)).limit(1);
|
||||
if (!firm?.stripeCustomerId) return reply.code(404).send({ error: 'no_customer' });
|
||||
|
||||
const stripe = getStripe();
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: firm.stripeCustomerId,
|
||||
return_url: `${env.PUBLIC_URL}/app/settings`,
|
||||
});
|
||||
return { url: session.url };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user