feat: billing/invoice details (full name, address, phone) under Settings > Billing (v0.76.0)

Adds a new user_billing_details table (1:1 with users, separate from the display name) with a form and GET/PUT API route. Full name is required by the form/API; address and phone stay optional. Not wired into Stripe invoicing yet — just captured for future use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-24 11:35:26 +02:00
parent 04a911b431
commit 003e8abe22
15 changed files with 6440 additions and 5 deletions
@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from "next/server";
import { db, userBillingDetails, eq } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { z } from "zod";
const PutSchema = z.object({
fullName: z.string().trim().min(1).max(200),
addressLine1: z.string().trim().max(200).optional(),
addressLine2: z.string().trim().max(200).optional(),
city: z.string().trim().max(120).optional(),
postalCode: z.string().trim().max(20).optional(),
country: z.string().trim().max(120).optional(),
phone: z.string().trim().max(40).optional(),
});
export async function GET() {
const { session, response } = await requireSession();
if (response) return response;
const details = await db.query.userBillingDetails.findFirst({
where: eq(userBillingDetails.userId, session!.user.id),
});
return NextResponse.json({ data: details ?? null });
}
export async function PUT(req: NextRequest) {
const { session, response } = await requireSession();
if (response) return response;
const parsed = PutSchema.safeParse(await req.json());
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
const body = parsed.data;
const userId = session!.user.id;
await db
.insert(userBillingDetails)
.values({ id: crypto.randomUUID(), userId, ...body, updatedAt: new Date() })
.onConflictDoUpdate({
target: userBillingDetails.userId,
set: { ...body, updatedAt: new Date() },
});
return NextResponse.json({ ok: true });
}