"use server"; import { createAdminClient } from "@/lib/supabase/server"; import { auth } from "@clerk/nextjs/server"; import { reportUsage } from "@/lib/supabase/stripe-actions"; import type { PayrollRunSummary, PayrollRun, PayslipWithEmployee, CreatePayrollRunInput, } from "@/lib/supabase/payroll-types"; // ── Helpers ────────────────────────────────────────────────── 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 fetchPayrollRuns(): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data: runs, error } = await supabase .from("payroll_runs") .select("*") .eq("organisation_id", orgId) .order("created_at", { ascending: false }); if (error) throw error; if (!runs) return []; // Get employee counts for each run const runIds = runs.map((r: any) => r.id); let counts: Record = {}; if (runIds.length > 0) { const { data: countData } = await supabase .from("payslips") .select("payroll_run_id") .in("payroll_run_id", runIds); counts = {}; countData?.forEach((p) => { counts[p.payroll_run_id] = (counts[p.payroll_run_id] || 0) + 1; }); } return runs.map((r: any) => ({ id: r.id, period_start: r.period_start, period_end: r.period_end, pay_frequency: r.pay_frequency, status: r.status, jurisdiction_name: r.jurisdiction_name, jurisdiction_country_code: r.jurisdiction_country_code, total_gross: Number(r.total_gross), total_net: Number(r.total_net), employee_count: counts[r.id] || 0, created_at: r.created_at, })); } export async function fetchPayrollRun(runId: string): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("payroll_runs") .select("*") .eq("id", runId) .eq("organisation_id", orgId) .single(); if (error) return null; return data as PayrollRun; } export async function fetchPayslipsForRun(runId: string): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("payslips") .select( ` *, employee:employees(id, first_name, last_name, email, job_title) ` ) .eq("payroll_run_id", runId) .order("employee.first_name", { ascending: true }); if (error) throw error; return (data || []).map((p: any) => ({ ...p, gross_pay: Number(p.gross_pay), total_deductions: Number(p.total_deductions), net_pay: Number(p.net_pay), employer_contributions: Number(p.employer_contributions), deduction_lines: (p.deduction_lines || []) as any[], employer_contribution_lines: (p.employer_contribution_lines || []) as any[], hours_worked: p.hours_worked ? Number(p.hours_worked) : null, hourly_rate: p.hourly_rate ? Number(p.hourly_rate) : null, employee: p.employee ? { id: p.employee.id, first_name: p.employee.first_name, last_name: p.employee.last_name, email: p.employee.email, job_title: p.employee.job_title, } : null, })) as PayslipWithEmployee[]; } export async function fetchPayslip(runId: string, employeeId: string): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const { data, error } = await supabase .from("payslips") .select( ` *, employee:employees(id, first_name, last_name, email, job_title) ` ) .eq("payroll_run_id", runId) .eq("employee_id", employeeId) .single(); if (error) return null; return { ...data, gross_pay: Number(data.gross_pay), total_deductions: Number(data.total_deductions), net_pay: Number(data.net_pay), employer_contributions: Number(data.employer_contributions), deduction_lines: (data.deduction_lines || []) as any[], employer_contribution_lines: (data.employer_contribution_lines || []) as any[], hours_worked: data.hours_worked ? Number(data.hours_worked) : null, hourly_rate: data.hourly_rate ? Number(data.hourly_rate) : null, employee: data.employee ? { id: data.employee.id, first_name: data.employee.first_name, last_name: data.employee.last_name, email: data.employee.email, job_title: data.employee.job_title, } : null, } as PayslipWithEmployee; } // ── Server actions (writes) ────────────────────────────────── export async function confirmPayrollRun( runId: string ): 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("payroll_runs") .update({ status: "confirmed", confirmed_by: userId, confirmed_at: new Date().toISOString(), }) .eq("id", runId) .eq("organisation_id", orgId) .eq("status", "draft"); if (error) return { success: false, error: error.message }; return { success: true }; } export async function createPayrollRunWithPayslips( input: CreatePayrollRunInput, jurisdictionCode: string, jurisdictionName: string, payslips: { employee_id: string; gross_pay: number; total_deductions: number; net_pay: number; employer_contributions: number; deduction_lines: any[]; employer_contribution_lines: any[]; hours_worked?: number; hourly_rate?: number; }[] ): Promise<{ success: boolean; runId?: string; error?: string }> { const { userId } = await auth(); if (!userId) return { success: false, error: "Unauthenticated" }; const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const totalGross = payslips.reduce((s, p) => s + p.gross_pay, 0); const totalDeductions = payslips.reduce((s, p) => s + p.total_deductions, 0); const totalNet = payslips.reduce((s, p) => s + p.net_pay, 0); const totalEmployer = payslips.reduce((s, p) => s + p.employer_contributions, 0); // Create payroll run const { data: run, error: runError } = await supabase .from("payroll_runs") .insert({ organisation_id: orgId, period_start: input.period_start, period_end: input.period_end, pay_frequency: input.pay_frequency, status: "draft", jurisdiction_country_code: jurisdictionCode, jurisdiction_name: jurisdictionName, total_gross: Math.round(totalGross * 100) / 100, total_deductions: Math.round(totalDeductions * 100) / 100, total_net: Math.round(totalNet * 100) / 100, total_employer_contributions: Math.round(totalEmployer * 100) / 100, created_by: userId, }) .select("id") .single(); if (runError || !run) { return { success: false, error: runError?.message || "Failed to create payroll run" }; } // Create payslips const payslipRecords = payslips.map((p) => ({ payroll_run_id: run.id, employee_id: p.employee_id, organisation_id: orgId, gross_pay: Math.round(p.gross_pay * 100) / 100, total_deductions: Math.round(p.total_deductions * 100) / 100, net_pay: Math.round(p.net_pay * 100) / 100, employer_contributions: Math.round(p.employer_contributions * 100) / 100, deduction_lines: p.deduction_lines, employer_contribution_lines: p.employer_contribution_lines, hours_worked: p.hours_worked || null, hourly_rate: p.hourly_rate || null, })); if (payslipRecords.length > 0) { const { error: psError } = await supabase .from("payslips") .insert(payslipRecords); if (psError) { // Rollback: delete the payroll run await supabase.from("payroll_runs").delete().eq("id", run.id); return { success: false, error: psError.message }; } } // Report usage for PAYG billing (non-blocking) reportUsage("payroll-runs", 1).catch(() => {}); return { success: true, runId: run.id }; }