4d7f00be1f
- 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>
149 lines
4.7 KiB
TypeScript
149 lines
4.7 KiB
TypeScript
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);
|
|
if (authResult instanceof NextResponse) return authResult;
|
|
|
|
const supabase = getSupabaseAdmin();
|
|
const { searchParams } = new URL(request.url);
|
|
const orgId = searchParams.get("orgId");
|
|
const page = parseInt(searchParams.get("page") || "1");
|
|
const limit = parseInt(searchParams.get("limit") || "20");
|
|
const offset = (page - 1) * limit;
|
|
|
|
try {
|
|
if (orgId) {
|
|
// Get specific org's credit balance and transaction history
|
|
const [orgResult, transactionsResult] = await Promise.all([
|
|
supabase
|
|
.from("organizations")
|
|
.select("id, name, credit_balance")
|
|
.eq("id", orgId)
|
|
.single(),
|
|
supabase
|
|
.from("credit_transactions")
|
|
.select(
|
|
"*, creator:users!credit_transactions_created_by_fkey(id, email, name)",
|
|
{ count: "exact" }
|
|
)
|
|
.eq("organization_id", orgId)
|
|
.order("created_at", { ascending: false })
|
|
.range(offset, offset + limit - 1),
|
|
]);
|
|
|
|
if (orgResult.error) throw orgResult.error;
|
|
|
|
return NextResponse.json({
|
|
organization: orgResult.data,
|
|
transactions: (transactionsResult.data || []) as unknown as CreditTransaction[],
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total: transactionsResult.count || 0,
|
|
pages: Math.ceil((transactionsResult.count || 0) / limit),
|
|
},
|
|
});
|
|
}
|
|
|
|
// List all organizations with credit balances
|
|
const { data: orgs, count, error } = await supabase
|
|
.from("organizations")
|
|
.select("id, name, credit_balance, subscription_tier", { count: "exact" })
|
|
.order("credit_balance", { ascending: false })
|
|
.range(offset, offset + limit - 1);
|
|
|
|
if (error) throw error;
|
|
|
|
return NextResponse.json({
|
|
organizations: orgs || [],
|
|
pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit) },
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching credits:", error);
|
|
return NextResponse.json({ error: "Failed to fetch credits" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const authResult = await requireAdmin(request);
|
|
if (authResult instanceof NextResponse) return authResult;
|
|
|
|
const supabase = getSupabaseAdmin();
|
|
|
|
try {
|
|
const body = await request.json();
|
|
const { organization_id, amount, type, reason, notes } = body;
|
|
|
|
if (!organization_id || amount === undefined || !type || !reason) {
|
|
return NextResponse.json(
|
|
{ error: "organization_id, amount, type, and reason are required" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (!["credit", "debit"].includes(type)) {
|
|
return NextResponse.json({ error: "type must be 'credit' or 'debit'" }, { status: 400 });
|
|
}
|
|
|
|
// Get current balance
|
|
const { data: org, error: orgError } = await supabase
|
|
.from("organizations")
|
|
.select("id, name, credit_balance")
|
|
.eq("id", organization_id)
|
|
.single();
|
|
|
|
if (orgError || !org) {
|
|
return NextResponse.json({ error: "Organization not found" }, { status: 404 });
|
|
}
|
|
|
|
const absAmount = Math.abs(Math.round(amount));
|
|
const signedAmount = type === "credit" ? absAmount : -absAmount;
|
|
const currentBalance = typeof org.credit_balance === "number" ? org.credit_balance : 0;
|
|
const newBalance = currentBalance + signedAmount;
|
|
|
|
if (newBalance < 0) {
|
|
return NextResponse.json(
|
|
{ error: `Insufficient balance. Current: ${currentBalance}, debit: ${absAmount}` },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Insert transaction and update balance atomically
|
|
const { data, error: txError } = await supabase
|
|
.from("credit_transactions")
|
|
.insert({
|
|
organization_id,
|
|
amount: signedAmount,
|
|
balance_after: newBalance,
|
|
type,
|
|
reason,
|
|
notes,
|
|
created_by: authResult.userId,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (txError) throw txError;
|
|
const transaction = data as unknown as CreditTransaction;
|
|
|
|
const { error: updateError } = await supabase
|
|
.from("organizations")
|
|
.update({ credit_balance: newBalance })
|
|
.eq("id", organization_id);
|
|
|
|
if (updateError) throw updateError;
|
|
|
|
return NextResponse.json({
|
|
transaction,
|
|
new_balance: newBalance,
|
|
organization: { id: org.id, name: org.name },
|
|
});
|
|
} catch (error) {
|
|
console.error("Error processing credit transaction:", error);
|
|
return NextResponse.json({ error: "Failed to process credit transaction" }, { status: 500 });
|
|
}
|
|
}
|