a67fc0767e
The order-action / order-block UI extensions are hosted on extensions.shopifycdn.com and call our app via fetch(). Without CORS headers the browser blocked the response. authenticate.admin already returns a cors helper and handles OPTIONS preflight - wrap every Response with it.
105 lines
3.3 KiB
TypeScript
105 lines
3.3 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";
|
|
|
|
/**
|
|
* 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 orderGid = orderId.startsWith("gid://")
|
|
? orderId
|
|
: `gid://shopify/Order/${orderId}`;
|
|
|
|
const invoices = await db.invoice.findMany({
|
|
where: { shopDomain: session.shop, orderId: orderGid },
|
|
orderBy: [{ issuedAt: "desc" }],
|
|
});
|
|
const latest = invoices.find((i) => i.kind === "invoice" && !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");
|
|
if (!op) {
|
|
// Also accept the action 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 = (form.get("action") as string | null) ?? null;
|
|
}
|
|
}
|
|
op = op ?? "generate";
|
|
|
|
try {
|
|
if (op === "cancel_reissue") {
|
|
const result = await cancelAndReissue({
|
|
shopDomain: session.shop,
|
|
admin,
|
|
orderId,
|
|
});
|
|
return cors(Response.json({ ok: true, op, ...result }));
|
|
}
|
|
|
|
const result = await generateInvoice({
|
|
shopDomain: session.shop,
|
|
admin,
|
|
orderId,
|
|
});
|
|
return cors(Response.json({ ok: true, op: "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(),
|
|
};
|
|
}
|