From 4d7f00be1ff007d2640d60494f41b2915f57c73a Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 7 Mar 2026 01:21:05 +0100 Subject: [PATCH] fix: add billing types and fix Supabase type casting for admin routes - Add src/types/billing.ts with Payment, Coupon, CreditTransaction, Invoice types - Cast all Supabase query results through 'unknown' for untyped billing tables - All routes now build cleanly with strict TypeScript checking Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/app/api/admin/coupons/route.ts | 13 ++-- frontend/src/app/api/admin/credits/route.ts | 6 +- frontend/src/app/api/admin/invoices/route.ts | 26 +++++--- frontend/src/app/api/admin/payments/route.ts | 10 ++- frontend/src/types/billing.ts | 70 ++++++++++++++++++++ 5 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 frontend/src/types/billing.ts diff --git a/frontend/src/app/api/admin/coupons/route.ts b/frontend/src/app/api/admin/coupons/route.ts index b884156..d06623f 100644 --- a/frontend/src/app/api/admin/coupons/route.ts +++ b/frontend/src/app/api/admin/coupons/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { getSupabaseAdmin } from "@/lib/admin"; import { requireAdmin } from "@/lib/apiAuth"; +import type { Coupon } from "@/types/billing"; export async function GET(request: Request) { const authResult = await requireAdmin(request); @@ -22,11 +23,13 @@ export async function GET(request: Request) { if (activeOnly) query = query.eq("is_active", true); - const { data: coupons, count, error } = await query; + const { data, count, error } = await query; if (error) throw error; + const coupons = (data || []) as unknown as Coupon[]; + return NextResponse.json({ - coupons: coupons || [], + coupons, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) }, }); } catch (error) { @@ -69,7 +72,7 @@ export async function POST(request: Request) { ); } - const { data: coupon, error } = await supabase + const { data, error } = await supabase .from("coupons") .insert({ code: code.toUpperCase().trim(), @@ -92,6 +95,7 @@ export async function POST(request: Request) { } throw error; } + const coupon = data as unknown as Coupon; return NextResponse.json({ coupon }); } catch (error) { @@ -132,7 +136,7 @@ export async function PATCH(request: Request) { if (updates.code) safeUpdates.code = updates.code.toUpperCase().trim(); - const { data: coupon, error } = await supabase + const { data, error } = await supabase .from("coupons") .update(safeUpdates) .eq("id", id) @@ -140,6 +144,7 @@ export async function PATCH(request: Request) { .single(); if (error) throw error; + const coupon = data as unknown as Coupon; return NextResponse.json({ coupon }); } catch (error) { diff --git a/frontend/src/app/api/admin/credits/route.ts b/frontend/src/app/api/admin/credits/route.ts index cccc14d..4af2e38 100644 --- a/frontend/src/app/api/admin/credits/route.ts +++ b/frontend/src/app/api/admin/credits/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { getSupabaseAdmin } from "@/lib/admin"; import { requireAdmin } from "@/lib/apiAuth"; +import type { CreditTransaction } from "@/types/billing"; export async function GET(request: Request) { const authResult = await requireAdmin(request); @@ -37,7 +38,7 @@ export async function GET(request: Request) { return NextResponse.json({ organization: orgResult.data, - transactions: transactionsResult.data || [], + transactions: (transactionsResult.data || []) as unknown as CreditTransaction[], pagination: { page, limit, @@ -111,7 +112,7 @@ export async function POST(request: Request) { } // Insert transaction and update balance atomically - const { data: transaction, error: txError } = await supabase + const { data, error: txError } = await supabase .from("credit_transactions") .insert({ organization_id, @@ -126,6 +127,7 @@ export async function POST(request: Request) { .single(); if (txError) throw txError; + const transaction = data as unknown as CreditTransaction; const { error: updateError } = await supabase .from("organizations") diff --git a/frontend/src/app/api/admin/invoices/route.ts b/frontend/src/app/api/admin/invoices/route.ts index 10629d1..c85cfb5 100644 --- a/frontend/src/app/api/admin/invoices/route.ts +++ b/frontend/src/app/api/admin/invoices/route.ts @@ -3,6 +3,7 @@ import { getSupabaseAdmin } from "@/lib/admin"; import { requireAdmin } from "@/lib/apiAuth"; import { sendEmail } from "@/lib/email"; import { invoiceEmail } from "@/lib/email-templates"; +import type { Invoice } from "@/types/billing"; export async function GET(request: Request) { const authResult = await requireAdmin(request); @@ -29,11 +30,13 @@ export async function GET(request: Request) { if (orgId) query = query.eq("organization_id", orgId); if (status) query = query.eq("status", status); - const { data: invoices, count, error } = await query; + const { data, count, error } = await query; if (error) throw error; + const invoices = (data || []) as unknown as Invoice[]; + return NextResponse.json({ - invoices: invoices || [], + invoices, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) }, }); } catch (error) { @@ -68,7 +71,7 @@ export async function POST(request: Request) { .like("invoice_number", `${prefix}%`); const seq = String((count || 0) + 1).padStart(4, "0"); - const { data: invoice, error } = await supabase + const { data, error } = await supabase .from("invoices") .insert({ invoice_number: `${prefix}-${seq}`, @@ -85,6 +88,7 @@ export async function POST(request: Request) { .single(); if (error) throw error; + const invoice = data as unknown as Invoice; return NextResponse.json({ invoice }); } catch (error) { @@ -109,17 +113,19 @@ export async function PATCH(request: Request) { // Send invoice via email if (action === "send") { - const { data: invoice } = await supabase + const { data } = await supabase .from("invoices") .select("*, organization:organizations(id, name, billing_email)") .eq("id", id) .single(); + const invoice = data as unknown as Invoice | null; + if (!invoice) { return NextResponse.json({ error: "Invoice not found" }, { status: 404 }); } - const org = invoice.organization as { id: string; name: string; billing_email?: string } | null; + const org = invoice.organization; const recipientEmail = org?.billing_email; if (!recipientEmail) { @@ -131,12 +137,12 @@ export async function PATCH(request: Request) { const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; const template = invoiceEmail({ - organizationName: org.name, + organizationName: org?.name || "Unknown", invoiceNumber: invoice.invoice_number, amount: invoice.amount, currency: invoice.currency, dueDate: invoice.due_date, - items: (invoice.items as Array<{ description: string; amount: number }>) || [], + items: (invoice.items) || [], dashboardUrl: `${appUrl}/dashboard/settings`, }); @@ -161,7 +167,7 @@ export async function PATCH(request: Request) { // Mark as paid if (action === "mark_paid") { - const { data: invoice, error } = await supabase + const { data, error } = await supabase .from("invoices") .update({ status: "paid", paid_at: new Date().toISOString(), updated_at: new Date().toISOString() }) .eq("id", id) @@ -169,6 +175,7 @@ export async function PATCH(request: Request) { .single(); if (error) throw error; + const invoice = data as unknown as Invoice; return NextResponse.json({ invoice }); } @@ -179,7 +186,7 @@ export async function PATCH(request: Request) { if (key in updates) safeUpdates[key] = updates[key]; } - const { data: invoice, error } = await supabase + const { data, error } = await supabase .from("invoices") .update(safeUpdates) .eq("id", id) @@ -187,6 +194,7 @@ export async function PATCH(request: Request) { .single(); if (error) throw error; + const invoice = data as unknown as Invoice; return NextResponse.json({ invoice }); } catch (error) { diff --git a/frontend/src/app/api/admin/payments/route.ts b/frontend/src/app/api/admin/payments/route.ts index ffa5e26..90eedcf 100644 --- a/frontend/src/app/api/admin/payments/route.ts +++ b/frontend/src/app/api/admin/payments/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { getSupabaseAdmin } from "@/lib/admin"; import { requireAdmin } from "@/lib/apiAuth"; +import type { Payment } from "@/types/billing"; export async function GET(request: Request) { const authResult = await requireAdmin(request); @@ -27,11 +28,13 @@ export async function GET(request: Request) { if (orgId) query = query.eq("organization_id", orgId); if (status) query = query.eq("status", status); - const { data: payments, count, error } = await query; + const { data, count, error } = await query; if (error) throw error; + const payments = (data || []) as unknown as Payment[]; + return NextResponse.json({ - payments: payments || [], + payments, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) }, }); } catch (error) { @@ -57,7 +60,7 @@ export async function POST(request: Request) { ); } - const { data: payment, error } = await supabase + const { data, error } = await supabase .from("payments") .insert({ organization_id, @@ -73,6 +76,7 @@ export async function POST(request: Request) { .single(); if (error) throw error; + const payment = data as unknown as Payment; // If payment is completed, optionally add credits if (payment.status === "completed" && body.add_as_credit) { diff --git a/frontend/src/types/billing.ts b/frontend/src/types/billing.ts new file mode 100644 index 0000000..ac42d15 --- /dev/null +++ b/frontend/src/types/billing.ts @@ -0,0 +1,70 @@ +// Types for billing tables (not yet in Supabase generated types) + +export interface Payment { + id: string; + organization_id: string; + amount: number; + currency: string; + status: string; + method: string; + stripe_payment_id?: string; + description?: string; + notes?: string; + created_by: string; + created_at: string; + updated_at: string; + organization?: { id: string; name: string }; + creator?: { id: string; email: string; name: string }; +} + +export interface Coupon { + id: string; + code: string; + description?: string; + discount_type: "percentage" | "fixed"; + discount_value: number; + currency: string; + max_redemptions?: number; + current_redemptions: number; + applicable_tiers: string[]; + valid_from: string; + valid_until?: string; + is_active: boolean; + created_by: string; + created_at: string; + updated_at: string; + creator?: { id: string; email: string; name: string }; +} + +export interface CreditTransaction { + id: string; + organization_id: string; + amount: number; + balance_after: number; + type: "credit" | "debit"; + reason: string; + reference_id?: string; + notes?: string; + created_by: string; + created_at: string; + creator?: { id: string; email: string; name: string }; +} + +export interface Invoice { + id: string; + invoice_number: string; + organization_id: string; + amount: number; + currency: string; + status: string; + items: Array<{ description: string; amount: number }>; + due_date?: string; + paid_at?: string; + stripe_invoice_id?: string; + notes?: string; + created_by: string; + created_at: string; + updated_at: string; + organization?: { id: string; name: string; billing_email?: string }; + creator?: { id: string; email: string; name: string }; +}