Files
linumiq-invoice/app/routes/webhooks.orders.create.tsx
T
Gerhard Scheikl dde53319e5 fix(observability,webhooks,i18n): timestamped logs, dedupe webhook retries, default non-de locales to English
- New custom server.js (replaces react-router-serve): ISO timestamps on
  all console.* output and on access logs, and skip successful /healthz
  polls so real traffic stays visible.
- New ProcessedWebhook table + dedupe helper keyed on
  X-Shopify-Webhook-Id; stops Shopify retries from triggering a second
  invoice email when the original delivery exceeded the 5s ack timeout.
- orders/create + orders/fulfilled now respond 200 immediately and run
  the PDF/email work in the background so we stay under that timeout.
- pickLanguage(): non-German locales (it, fr, es, ...) now default to
  English instead of falling back to German. Empty/unknown still maps to
  'de' so the per-shop defaultLanguage chain keeps working.
- Tests for pickLanguage and dedupe via node --test + tsx.
2026-05-15 11:02:17 +02:00

60 lines
2.4 KiB
TypeScript

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 { isDuplicateWebhook } 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}`);
// Drop Shopify retries we've already processed (prevents the duplicate
// invoice email we saw when the first delivery exceeded Shopify's 5s ack
// timeout — the work still completed, but Shopify resent the webhook).
if (await isDuplicateWebhook(request, shop, topic)) return new Response();
if (!session || !admin) return new Response();
const orderId = payload?.id;
if (orderId == null) 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. Dedupe
// above guarantees we don't double-process a retry while the first
// delivery is still working.
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) {
console.warn(
`auto-email (wire-transfer placed) failed for order ${orderId} on ${shop}: ${result.reason}`,
);
}
});
return new Response();
};