Files
linumiq-invoice/app/routes/api.orders.$orderId.invoice.tsx
2026-05-09 19:26:33 +02:00

156 lines
5.1 KiB
TypeScript

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(),
};
}