mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 20:51:17 +00:00
166 lines
6.5 KiB
TypeScript
166 lines
6.5 KiB
TypeScript
"use server";
|
|
|
|
import * as z from "zod";
|
|
import { revalidatePath } from "next/cache";
|
|
import { createClient } from "@/lib/supabase/server";
|
|
import { verifySession } from "@/lib/supabase/dal";
|
|
import { getClientId } from "@/modules/shared/queries/profile";
|
|
import { getInvoicesForPdf } from "@/modules/shared/queries/invoices";
|
|
import { getMailRecipient } from "@/modules/shared/queries/mail-recipient";
|
|
import { getAppUrl } from "@/modules/shared/mail/app-url";
|
|
import { sendMail } from "@/modules/shared/mail/send-mail";
|
|
import { invoiceMail } from "@/modules/shared/mail/templates";
|
|
import { invoiceFileName, renderInvoicePdf } from "@/modules/shared/pdf/render-invoice";
|
|
import {
|
|
INVOICE_STATUSES,
|
|
InvoiceDraftSchema,
|
|
firstIssueMessage,
|
|
type InvoiceDraft,
|
|
type InvoiceStatus,
|
|
} from "@/modules/shared/schemas/invoice";
|
|
import { invoiceTotals, toIsoDate } from "@/modules/shared/lib/invoice-math";
|
|
|
|
export type InvoiceActionState = { error: string } | { success: true };
|
|
export type CreateInvoiceState = { error: string } | { success: true; mailError?: string };
|
|
|
|
function revalidateBilling(profileId: string) {
|
|
revalidatePath("/client/billing");
|
|
revalidatePath(`/admin/clients/${profileId}/billing`);
|
|
}
|
|
|
|
// Versendet eine Rechnung als PDF an den Ansprechpartner. Aufrufer muss Admin-Rechte bereits geprüft haben.
|
|
async function deliverInvoiceMail(profileId: string, clientId: string, invoiceId: string): Promise<{ error: string } | null> {
|
|
const supabase = await createClient();
|
|
const { data: invoice } = await supabase
|
|
.from("invoices")
|
|
.select("id, status")
|
|
.eq("id", invoiceId)
|
|
.eq("client_id", clientId)
|
|
.maybeSingle();
|
|
|
|
if (!invoice) return { error: "Rechnung nicht gefunden." };
|
|
if (invoice.status === "cancelled") return { error: "Stornierte Rechnungen werden nicht verschickt." };
|
|
|
|
const [pdfData] = await getInvoicesForPdf([invoiceId]);
|
|
const recipient = await getMailRecipient(profileId);
|
|
if (!pdfData || !recipient) return { error: "Rechnung oder Empfänger konnte nicht geladen werden." };
|
|
|
|
const pdf = await renderInvoicePdf(pdfData);
|
|
const mail = invoiceMail({
|
|
name: recipient.name,
|
|
invoiceNumber: pdfData.invoiceNumber,
|
|
invoiceDate: pdfData.invoiceDate,
|
|
dueDate: pdfData.dueDate,
|
|
grossTotal: invoiceTotals(pdfData.items).gross,
|
|
appUrl: await getAppUrl(),
|
|
});
|
|
|
|
const sent = await sendMail({
|
|
to: recipient.email,
|
|
...mail,
|
|
type: "invoice",
|
|
attachments: [{ filename: invoiceFileName(pdfData.invoiceNumber), content: new Uint8Array(pdf), contentType: "application/pdf" }],
|
|
});
|
|
if (!sent.ok) return { error: sent.error };
|
|
|
|
await supabase
|
|
.from("invoices")
|
|
.update({ email_sent_at: new Date().toISOString(), email_sent_to: recipient.email })
|
|
.eq("id", invoiceId);
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function createInvoice(draft: InvoiceDraft, options: { sendEmail?: boolean } = {}): Promise<CreateInvoiceState> {
|
|
const session = await verifySession();
|
|
if (!session.isAdmin) return { error: "Keine Berechtigung." };
|
|
|
|
const validated = InvoiceDraftSchema.safeParse(draft);
|
|
if (!validated.success) return { error: firstIssueMessage(validated.error) };
|
|
|
|
const data = validated.data;
|
|
const clientId = await getClientId(data.profileId);
|
|
if (!clientId) return { error: "Client hat noch keine Stammdaten hinterlegt." };
|
|
|
|
const supabase = await createClient();
|
|
const { data: invoiceId, error } = await supabase.rpc("create_invoice", {
|
|
p_client_id: clientId,
|
|
p_invoice_date: data.invoiceDate,
|
|
p_due_date: data.dueDate,
|
|
p_company_name: data.companyName,
|
|
p_billing_street: data.billingStreet,
|
|
p_billing_postal_code: data.billingPostalCode,
|
|
p_billing_city: data.billingCity,
|
|
p_billing_country: data.billingCountry.toUpperCase(),
|
|
p_items: data.items.map((item) => ({
|
|
product_template_id: item.productTemplateId,
|
|
description: item.description,
|
|
quantity: item.quantity,
|
|
unit_price: item.unitPrice,
|
|
tax_rate: item.taxRate,
|
|
})),
|
|
});
|
|
|
|
if (error) return { error: "Rechnung konnte nicht erstellt werden." };
|
|
|
|
const mailFailure = options.sendEmail ? await deliverInvoiceMail(data.profileId, clientId, invoiceId) : null;
|
|
|
|
revalidateBilling(data.profileId);
|
|
return mailFailure ? { success: true, mailError: mailFailure.error } : { success: true };
|
|
}
|
|
|
|
const SendSchema = z.object({ profileId: z.string().uuid(), invoiceId: z.string().uuid() });
|
|
|
|
export async function sendInvoiceEmail(profileId: string, invoiceId: string): Promise<InvoiceActionState> {
|
|
const session = await verifySession();
|
|
if (!session.isAdmin) return { error: "Keine Berechtigung." };
|
|
|
|
const validated = SendSchema.safeParse({ profileId, invoiceId });
|
|
if (!validated.success) return { error: "Ungültige Rechnung." };
|
|
|
|
const clientId = await getClientId(validated.data.profileId);
|
|
if (!clientId) return { error: "Client hat noch keine Stammdaten hinterlegt." };
|
|
|
|
const failure = await deliverInvoiceMail(validated.data.profileId, clientId, validated.data.invoiceId);
|
|
if (failure) return failure;
|
|
|
|
revalidateBilling(validated.data.profileId);
|
|
return { success: true };
|
|
}
|
|
|
|
const StatusSchema = z.object({
|
|
profileId: z.string().uuid(),
|
|
invoiceId: z.string().uuid(),
|
|
status: z.enum(INVOICE_STATUSES),
|
|
});
|
|
|
|
export async function updateInvoiceStatus(
|
|
profileId: string,
|
|
invoiceId: string,
|
|
status: InvoiceStatus
|
|
): Promise<InvoiceActionState> {
|
|
const session = await verifySession();
|
|
if (!session.isAdmin) return { error: "Keine Berechtigung." };
|
|
|
|
const validated = StatusSchema.safeParse({ profileId, invoiceId, status });
|
|
if (!validated.success) return { error: "Ungültiger Status." };
|
|
|
|
const clientId = await getClientId(validated.data.profileId);
|
|
if (!clientId) return { error: "Status konnte nicht geändert werden." };
|
|
|
|
const supabase = await createClient();
|
|
const { data, error } = await supabase
|
|
.from("invoices")
|
|
.update({
|
|
status: validated.data.status,
|
|
paid_at: validated.data.status === "paid" ? toIsoDate(new Date()) : null,
|
|
})
|
|
.eq("id", validated.data.invoiceId)
|
|
.eq("client_id", clientId)
|
|
.select("id");
|
|
|
|
if (error || !data?.length) return { error: "Status konnte nicht geändert werden." };
|
|
|
|
revalidateBilling(validated.data.profileId);
|
|
return { success: true };
|
|
}
|