"use server"; import { createAdminClient } from "@/lib/supabase/server"; import { auth } from "@clerk/nextjs/server"; import { reportUsage } from "@/lib/supabase/stripe-actions"; import type { Expense, ExpenseCategory, ExpenseStatus, ExpenseWithCategory } from "@/lib/supabase/expenses-types"; async function getOrganisationId(): Promise { const { userId } = await auth(); if (!userId) throw new Error("Unauthenticated"); const supabase = await createAdminClient(); const { data: member, error } = await supabase .from("organisation_members") .select("organisation_id") .eq("user_id", userId) .single(); if (error || !member) throw new Error("No organisation found"); return member.organisation_id; } // ── Fetch functions ────────────────────────────────────────── export async function fetchExpenses(status: ExpenseStatus | "all" = "all", userId?: string): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); // Get current user's role const { data: member } = await supabase .from("organisation_members") .select("role") .eq("organisation_id", orgId) .eq("user_id", userId || (await auth()).userId) .single(); const isEmployee = member?.role === "employee"; let query = supabase .from("expenses") .select("*, category:expense_categories(name, color), employee:employees(first_name, last_name)") .eq("organisation_id", orgId); // Employees only see their own expenses if (isEmployee) { query = query.eq("created_by", (await auth()).userId); } if (status !== "all") query = query.eq("status", status); query = query.order("expense_date", { ascending: false }); const { data, error } = await query; if (error) throw error; return (data || []).map((e: any) => ({ ...e, amount: Number(e.amount), category: e.category ? { name: e.category.name, color: e.category.color } : null, employee: e.employee ? { first_name: e.employee.first_name, last_name: e.employee.last_name } : null, })) as ExpenseWithCategory[]; } export async function fetchExpenseCategories(): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("expense_categories") .select("*") .eq("organisation_id", orgId) .order("name", { ascending: true }); if (error) throw error; return (data || []) as ExpenseCategory[]; } export async function fetchExpensesSummary(): Promise<{ total: number; byStatus: Record; byCategory: { category: string; total: number }[]; recentTrend: { month: string; total: number }[]; }> { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data: expenses, error } = await supabase .from("expenses") .select("amount, status, expense_date, category:expense_categories(name)") .eq("organisation_id", orgId); if (error) throw error; if (!expenses || expenses.length === 0) { return { total: 0, byStatus: { draft: 0, submitted: 0, approved: 0, rejected: 0, reimbursed: 0 }, byCategory: [], recentTrend: [], }; } const byStatus: Record = { draft: 0, submitted: 0, approved: 0, rejected: 0, reimbursed: 0 }; const byCat: Record = {}; const monthlyTotals: Record = {}; const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; let total = 0; expenses.forEach((e: any) => { const amt = Number(e.amount); total += amt; byStatus[e.status] = (byStatus[e.status] || 0) + amt; const catName = (e.category as any)?.name || "Uncategorised"; byCat[catName] = (byCat[catName] || 0) + amt; const d = new Date(e.expense_date); const key = `${monthNames[d.getMonth()]} ${d.getFullYear()}`; monthlyTotals[key] = (monthlyTotals[key] || 0) + amt; }); const byCategory = Object.entries(byCat) .map(([category, catTotal]) => ({ category, total: Math.round(catTotal * 100) / 100 })) .sort((a, b) => b.total - a.total); const recentTrend = Object.entries(monthlyTotals) .slice(-6) .map(([month, monthTotal]) => ({ month, total: Math.round(monthTotal * 100) / 100 })); return { total: Math.round(total * 100) / 100, byStatus: byStatus as Record, byCategory, recentTrend, }; } // ── Write functions ────────────────────────────────────────── export async function createExpense(input: { amount: number; currency: string; category_id: string | null; employee_id: string | null; vendor: string | null; description: string | null; expense_date: string; status: ExpenseStatus; receipt_url: string | null; }): Promise<{ success: boolean; expenseId?: string; error?: string }> { const { userId } = await auth(); if (!userId) return { success: false, error: "Unauthenticated" }; const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("expenses") .insert({ organisation_id: orgId, amount: Math.round(input.amount * 100) / 100, currency: input.currency, category_id: input.category_id || null, employee_id: input.employee_id || null, vendor: input.vendor, description: input.description, expense_date: input.expense_date, status: input.status, receipt_url: input.receipt_url, created_by: userId, }) .select("id") .single(); if (error) return { success: false, error: error.message }; // Report usage for PAYG billing (non-blocking) reportUsage("expenses", 1).catch(() => {}); return { success: true, expenseId: data.id }; } export async function updateExpenseStatus( expenseId: string, status: ExpenseStatus ): Promise<{ success: boolean; error?: string }> { const { userId } = await auth(); if (!userId) return { success: false, error: "Unauthenticated" }; const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { error } = await supabase .from("expenses") .update({ status, updated_at: new Date().toISOString() }) .eq("id", expenseId) .eq("organisation_id", orgId); if (error) return { success: false, error: error.message }; return { success: true }; } export async function deleteExpense(expenseId: string): Promise<{ success: boolean; error?: string }> { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { error } = await supabase .from("expenses") .delete() .eq("id", expenseId) .eq("organisation_id", orgId); if (error) return { success: false, error: error.message }; return { success: true }; } // ── Receipt capture ────────────────────────────────────────── async function getOrgAndUserId(): Promise<{ orgId: string; userId: string }> { const { userId } = await auth(); if (!userId) throw new Error("Unauthenticated"); const supabase = await createAdminClient(); const { data: member, error } = await supabase .from("organisation_members") .select("organisation_id") .eq("user_id", userId) .single(); if (error || !member) throw new Error("No organisation found"); return { orgId: member.organisation_id, userId }; } export async function uploadReceipt(file: File): Promise<{ success: boolean; url?: string; error?: string }> { const { orgId, userId } = await getOrgAndUserId(); const supabase = await createAdminClient(); const ext = file.name.split(".").pop(); const path = `${orgId}/${userId}/${Date.now()}-receipt.${ext}`; const { error } = await supabase.storage.from("receipts").upload(path, file); if (error) return { success: false, error: error.message }; const { data } = await supabase.storage.from("receipts").createSignedUrl(path, 86400 * 365); return { success: true, url: data?.signedUrl }; } export async function extractReceiptData(base64Image: string): Promise<{ success: boolean; data?: { merchant: string; amount: string; currency: string; date: string }; error?: string }> { try { const { GoogleGenerativeAI } = await import("@google/generative-ai"); const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" }); const result = await model.generateContent([ { inlineData: { data: base64Image, mimeType: "image/jpeg" } }, "Extract the merchant name, total amount, currency, and date from this receipt. Return JSON only: { merchant, amount, currency, date }" ]); const text = result.response.text(); const parsed = JSON.parse(text); return { success: true, data: parsed }; } catch (e: any) { return { success: false, error: e.message }; } }