import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router"; import { authenticate } from "../shopify.server"; import db from "../db.server"; import { generateInvoice } from "../services/invoice/generateInvoice.server"; import { cancelAndReissue } from "../services/invoice/cancelAndReissue.server"; import { sendInvoiceEmail } from "../services/invoice/email.server"; /** * GET /api/orders/:orderId/invoice → returns latest invoice metadata + history * POST /api/orders/:orderId/invoice → generates (or regenerates) the invoice * * `orderId` may be a numeric Shopify order id or a full GID; the generator * normalises it. */ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const { session, cors } = await authenticate.admin(request); const orderId = requireOrderId(params); const url = new URL(request.url); const kind = (url.searchParams.get("kind") === "offer" ? "offer" : "invoice") as "invoice" | "offer"; const orderGid = orderId.startsWith("gid://") ? orderId : `gid://shopify/${kind === "offer" ? "DraftOrder" : "Order"}/${orderId}`; const invoices = await db.invoice.findMany({ where: { shopDomain: session.shop, orderId: orderGid }, orderBy: [{ issuedAt: "desc" }], }); const latest = invoices.find((i) => i.kind === kind && !i.cancelledAt); return cors( Response.json({ latest: latest ? serialise(latest) : null, history: invoices.map(serialise), }), ); }; export const action = async ({ request, params }: ActionFunctionArgs) => { const { admin, session, cors } = await authenticate.admin(request); if (request.method !== "POST") { return cors(new Response("Method Not Allowed", { status: 405 })); } const orderId = requireOrderId(params); const url = new URL(request.url); let op = url.searchParams.get("action"); let kindParam = url.searchParams.get("kind"); if (!op || !kindParam) { // Also accept the action / kind from the form body (used by the in-app fetcher). const ct = request.headers.get("content-type") || ""; if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) { const form = await request.formData(); op = op ?? ((form.get("action") as string | null) ?? null); kindParam = kindParam ?? ((form.get("kind") as string | null) ?? null); } } op = op ?? "generate"; const kind: "invoice" | "offer" = kindParam === "offer" ? "offer" : "invoice"; try { if (op === "cancel_reissue") { const result = await cancelAndReissue({ shopDomain: session.shop, admin, orderId, }); return cors(Response.json({ ok: true, op, ...result })); } if (op === "send") { const orderGid = orderId.startsWith("gid://") ? orderId : `gid://shopify/Order/${orderId}`; let invoice = await db.invoice.findFirst({ where: { shopDomain: session.shop, orderId: orderGid, kind: "invoice", cancelledAt: null, }, orderBy: [{ version: "desc" }, { createdAt: "desc" }], }); if (!invoice) { const generated = await generateInvoice({ shopDomain: session.shop, admin, orderId, }); invoice = await db.invoice.findUnique({ where: { id: generated.invoiceId } }); } if (!invoice) throw new Error("Failed to materialise an invoice for this order."); const sendResult = await sendInvoiceEmail({ shopDomain: session.shop, invoiceId: invoice.id, }); if (!sendResult.ok) { return cors( Response.json( { ok: false, op: "send", error: sendResult.errorMessage ?? "Email send failed." }, { status: 422 }, ), ); } return cors( Response.json({ ok: true, op: "send", invoiceNumber: invoice.invoiceNumber, toAddress: sendResult.toAddress, }), ); } const result = await generateInvoice({ shopDomain: session.shop, admin, orderId, kind, }); return cors(Response.json({ ok: true, op: kind === "offer" ? "generate_offer" : "generate", ...result })); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error("invoice action failed:", err); return cors(Response.json({ ok: false, error: message }, { status: 400 })); } }; function requireOrderId(params: { orderId?: string }): string { const id = params.orderId; if (!id) throw new Response("orderId is required", { status: 400 }); return id; } function serialise(invoice: { id: string; invoiceNumber: string; version: number; kind: string; pdfUrl: string; status: string; sentAt: Date | null; cancelledAt: Date | null; issuedAt: Date; }) { return { id: invoice.id, invoiceNumber: invoice.invoiceNumber, version: invoice.version, kind: invoice.kind, pdfUrl: invoice.pdfUrl, status: invoice.status, sentAt: invoice.sentAt?.toISOString() ?? null, cancelledAt: invoice.cancelledAt?.toISOString() ?? null, issuedAt: invoice.issuedAt.toISOString(), }; }