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:
@@ -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) {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user