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/fulfilled — Automation 2: when an order is fulfilled and is NOT a * wire-transfer (manual-payment-gateway) order, automatically email the * invoice to the customer. Manual-gateway orders are intentionally skipped * because Automation 1 already emailed them at order-create time. */ export const action = async ({ request }: ActionFunctionArgs) => { const { shop, topic, payload, session, admin } = await authenticate.webhook(request); console.log(`Received ${topic} webhook for ${shop}`); // Reserve/commit dedupe — see webhooks/dedupe.server.ts. `null` => already // done/in-flight; commit on success / release on failure happen in the // background queue so a failed send is retried by Shopify, not dropped. const reservation = await reserveWebhook(request, shop, topic); if (!reservation) return new Response(); if (!session || !admin) { // App was 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 fast; do the heavy lifting after the response (see notes in // webhooks.orders.create.tsx for the rationale). runWebhookInBackground( `${topic} order=${orderId} shop=${shop}`, async () => { const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } }); if (!settings?.autoEmailOnFulfilledNonWireTransfer) return; const orderGid = `gid://shopify/Order/${String(orderId).replace(/^.*\//, "")}`; if (await isManualPaymentOrder(admin, orderGid)) { // Manual / wire-transfer order — handled by Automation 1, skip here. return; } const result = await generateAndEmailInvoice({ shopDomain: shop, admin, orderId, customerLocale, }); if (!result.ok) { // Throw so the reservation is released and Shopify retries. throw new Error( `auto-email (fulfilled) failed for order ${orderId} on ${shop}: ${result.reason}`, ); } }, reservation, ); return new Response(); };