import type { ActionFunctionArgs } from "react-router"; import { authenticate } from "../shopify.server"; import db from "../db.server"; import { generateAndEmailInvoice, isManualPaymentOrder, } from "../services/invoice/automations.server"; import { reserveWebhook } from "../services/webhooks/dedupe.server"; import { runWebhookInBackground } from "../services/webhooks/background.server"; /** * orders/create — Automation 1: when a wire-transfer (manual-payment-gateway) * order is placed, immediately generate and email the invoice (which includes * the bank details + GiroCode) so the customer can pay. Other orders are * ignored here; they're handled by orders/fulfilled (Automation 2). */ export const action = async ({ request }: ActionFunctionArgs) => { const { shop, topic, payload, session, admin } = await authenticate.webhook(request); console.log(`Received ${topic} webhook for ${shop}`); // Reserve this delivery (status="processing"). `null` => already // done/in-flight, so short-circuit. The reservation is committed only after // the background work succeeds, and released on failure so Shopify's retry // re-runs it (prevents the silent invoice loss we'd get if we recorded the // id as processed before the slow PDF/email work). const reservation = await reserveWebhook(request, shop, topic); if (!reservation) return new Response(); if (!session || !admin) { // App uninstalled before the webhook drained — nothing to do. await reservation.commit(); return new Response(); } const orderId = payload?.id; if (orderId == null) { await reservation.commit(); return new Response(); } const customerLocale = typeof payload?.customer_locale === "string" ? payload.customer_locale : undefined; // Respond 200 immediately and run the (slow) PDF + email work in the // background — keeps us well under Shopify's ~5s ack timeout. The queue // commits the reservation on success and releases it on failure. runWebhookInBackground( `${topic} order=${orderId} shop=${shop}`, async () => { const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } }); if (!settings?.autoEmailOnWireTransferPlaced) return; const orderGid = `gid://shopify/Order/${String(orderId).replace(/^.*\//, "")}`; if (!(await isManualPaymentOrder(admin, orderGid))) return; const result = await generateAndEmailInvoice({ shopDomain: shop, admin, orderId, customerLocale, }); if (!result.ok) { // Throw so the reservation is released and Shopify retries — don't // swallow the failure (which would leave the invoice unsent forever). throw new Error( `auto-email (wire-transfer placed) failed for order ${orderId} on ${shop}: ${result.reason}`, ); } }, reservation, ); return new Response(); };