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>
This commit is contained in:
Dennis
2026-03-07 01:21:05 +01:00
parent 5ebea6b0d6
commit 4d7f00be1f
5 changed files with 107 additions and 18 deletions
+9 -4
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { getSupabaseAdmin } from "@/lib/admin"; import { getSupabaseAdmin } from "@/lib/admin";
import { requireAdmin } from "@/lib/apiAuth"; import { requireAdmin } from "@/lib/apiAuth";
import type { Coupon } from "@/types/billing";
export async function GET(request: Request) { export async function GET(request: Request) {
const authResult = await requireAdmin(request); const authResult = await requireAdmin(request);
@@ -22,11 +23,13 @@ export async function GET(request: Request) {
if (activeOnly) query = query.eq("is_active", true); 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; if (error) throw error;
const coupons = (data || []) as unknown as Coupon[];
return NextResponse.json({ return NextResponse.json({
coupons: coupons || [], coupons,
pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) }, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) },
}); });
} catch (error) { } 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") .from("coupons")
.insert({ .insert({
code: code.toUpperCase().trim(), code: code.toUpperCase().trim(),
@@ -92,6 +95,7 @@ export async function POST(request: Request) {
} }
throw error; throw error;
} }
const coupon = data as unknown as Coupon;
return NextResponse.json({ coupon }); return NextResponse.json({ coupon });
} catch (error) { } catch (error) {
@@ -132,7 +136,7 @@ export async function PATCH(request: Request) {
if (updates.code) safeUpdates.code = updates.code.toUpperCase().trim(); if (updates.code) safeUpdates.code = updates.code.toUpperCase().trim();
const { data: coupon, error } = await supabase const { data, error } = await supabase
.from("coupons") .from("coupons")
.update(safeUpdates) .update(safeUpdates)
.eq("id", id) .eq("id", id)
@@ -140,6 +144,7 @@ export async function PATCH(request: Request) {
.single(); .single();
if (error) throw error; if (error) throw error;
const coupon = data as unknown as Coupon;
return NextResponse.json({ coupon }); return NextResponse.json({ coupon });
} catch (error) { } catch (error) {
+4 -2
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { getSupabaseAdmin } from "@/lib/admin"; import { getSupabaseAdmin } from "@/lib/admin";
import { requireAdmin } from "@/lib/apiAuth"; import { requireAdmin } from "@/lib/apiAuth";
import type { CreditTransaction } from "@/types/billing";
export async function GET(request: Request) { export async function GET(request: Request) {
const authResult = await requireAdmin(request); const authResult = await requireAdmin(request);
@@ -37,7 +38,7 @@ export async function GET(request: Request) {
return NextResponse.json({ return NextResponse.json({
organization: orgResult.data, organization: orgResult.data,
transactions: transactionsResult.data || [], transactions: (transactionsResult.data || []) as unknown as CreditTransaction[],
pagination: { pagination: {
page, page,
limit, limit,
@@ -111,7 +112,7 @@ export async function POST(request: Request) {
} }
// Insert transaction and update balance atomically // Insert transaction and update balance atomically
const { data: transaction, error: txError } = await supabase const { data, error: txError } = await supabase
.from("credit_transactions") .from("credit_transactions")
.insert({ .insert({
organization_id, organization_id,
@@ -126,6 +127,7 @@ export async function POST(request: Request) {
.single(); .single();
if (txError) throw txError; if (txError) throw txError;
const transaction = data as unknown as CreditTransaction;
const { error: updateError } = await supabase const { error: updateError } = await supabase
.from("organizations") .from("organizations")
+17 -9
View File
@@ -3,6 +3,7 @@ import { getSupabaseAdmin } from "@/lib/admin";
import { requireAdmin } from "@/lib/apiAuth"; import { requireAdmin } from "@/lib/apiAuth";
import { sendEmail } from "@/lib/email"; import { sendEmail } from "@/lib/email";
import { invoiceEmail } from "@/lib/email-templates"; import { invoiceEmail } from "@/lib/email-templates";
import type { Invoice } from "@/types/billing";
export async function GET(request: Request) { export async function GET(request: Request) {
const authResult = await requireAdmin(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 (orgId) query = query.eq("organization_id", orgId);
if (status) query = query.eq("status", status); if (status) query = query.eq("status", status);
const { data: invoices, count, error } = await query; const { data, count, error } = await query;
if (error) throw error; if (error) throw error;
const invoices = (data || []) as unknown as Invoice[];
return NextResponse.json({ return NextResponse.json({
invoices: invoices || [], invoices,
pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) }, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) },
}); });
} catch (error) { } catch (error) {
@@ -68,7 +71,7 @@ export async function POST(request: Request) {
.like("invoice_number", `${prefix}%`); .like("invoice_number", `${prefix}%`);
const seq = String((count || 0) + 1).padStart(4, "0"); const seq = String((count || 0) + 1).padStart(4, "0");
const { data: invoice, error } = await supabase const { data, error } = await supabase
.from("invoices") .from("invoices")
.insert({ .insert({
invoice_number: `${prefix}-${seq}`, invoice_number: `${prefix}-${seq}`,
@@ -85,6 +88,7 @@ export async function POST(request: Request) {
.single(); .single();
if (error) throw error; if (error) throw error;
const invoice = data as unknown as Invoice;
return NextResponse.json({ invoice }); return NextResponse.json({ invoice });
} catch (error) { } catch (error) {
@@ -109,17 +113,19 @@ export async function PATCH(request: Request) {
// Send invoice via email // Send invoice via email
if (action === "send") { if (action === "send") {
const { data: invoice } = await supabase const { data } = await supabase
.from("invoices") .from("invoices")
.select("*, organization:organizations(id, name, billing_email)") .select("*, organization:organizations(id, name, billing_email)")
.eq("id", id) .eq("id", id)
.single(); .single();
const invoice = data as unknown as Invoice | null;
if (!invoice) { if (!invoice) {
return NextResponse.json({ error: "Invoice not found" }, { status: 404 }); 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; const recipientEmail = org?.billing_email;
if (!recipientEmail) { if (!recipientEmail) {
@@ -131,12 +137,12 @@ export async function PATCH(request: Request) {
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
const template = invoiceEmail({ const template = invoiceEmail({
organizationName: org.name, organizationName: org?.name || "Unknown",
invoiceNumber: invoice.invoice_number, invoiceNumber: invoice.invoice_number,
amount: invoice.amount, amount: invoice.amount,
currency: invoice.currency, currency: invoice.currency,
dueDate: invoice.due_date, dueDate: invoice.due_date,
items: (invoice.items as Array<{ description: string; amount: number }>) || [], items: (invoice.items) || [],
dashboardUrl: `${appUrl}/dashboard/settings`, dashboardUrl: `${appUrl}/dashboard/settings`,
}); });
@@ -161,7 +167,7 @@ export async function PATCH(request: Request) {
// Mark as paid // Mark as paid
if (action === "mark_paid") { if (action === "mark_paid") {
const { data: invoice, error } = await supabase const { data, error } = await supabase
.from("invoices") .from("invoices")
.update({ status: "paid", paid_at: new Date().toISOString(), updated_at: new Date().toISOString() }) .update({ status: "paid", paid_at: new Date().toISOString(), updated_at: new Date().toISOString() })
.eq("id", id) .eq("id", id)
@@ -169,6 +175,7 @@ export async function PATCH(request: Request) {
.single(); .single();
if (error) throw error; if (error) throw error;
const invoice = data as unknown as Invoice;
return NextResponse.json({ invoice }); return NextResponse.json({ invoice });
} }
@@ -179,7 +186,7 @@ export async function PATCH(request: Request) {
if (key in updates) safeUpdates[key] = updates[key]; if (key in updates) safeUpdates[key] = updates[key];
} }
const { data: invoice, error } = await supabase const { data, error } = await supabase
.from("invoices") .from("invoices")
.update(safeUpdates) .update(safeUpdates)
.eq("id", id) .eq("id", id)
@@ -187,6 +194,7 @@ export async function PATCH(request: Request) {
.single(); .single();
if (error) throw error; if (error) throw error;
const invoice = data as unknown as Invoice;
return NextResponse.json({ invoice }); return NextResponse.json({ invoice });
} catch (error) { } catch (error) {
+7 -3
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { getSupabaseAdmin } from "@/lib/admin"; import { getSupabaseAdmin } from "@/lib/admin";
import { requireAdmin } from "@/lib/apiAuth"; import { requireAdmin } from "@/lib/apiAuth";
import type { Payment } from "@/types/billing";
export async function GET(request: Request) { export async function GET(request: Request) {
const authResult = await requireAdmin(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 (orgId) query = query.eq("organization_id", orgId);
if (status) query = query.eq("status", status); if (status) query = query.eq("status", status);
const { data: payments, count, error } = await query; const { data, count, error } = await query;
if (error) throw error; if (error) throw error;
const payments = (data || []) as unknown as Payment[];
return NextResponse.json({ return NextResponse.json({
payments: payments || [], payments,
pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) }, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) },
}); });
} catch (error) { } 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") .from("payments")
.insert({ .insert({
organization_id, organization_id,
@@ -73,6 +76,7 @@ export async function POST(request: Request) {
.single(); .single();
if (error) throw error; if (error) throw error;
const payment = data as unknown as Payment;
// If payment is completed, optionally add credits // If payment is completed, optionally add credits
if (payment.status === "completed" && body.add_as_credit) { if (payment.status === "completed" && body.add_as_credit) {
+70
View File
@@ -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 };
}