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.
This commit is contained in:
Gerhard Scheikl
2026-05-15 11:02:17 +02:00
parent 274ccfbc01
commit dde53319e5
12 changed files with 361 additions and 29 deletions
+21 -13
View File
@@ -5,6 +5,8 @@ import {
generateAndEmailInvoice,
isManualPaymentOrder,
} from "../services/invoice/automations.server";
import { isDuplicateWebhook } 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
@@ -16,36 +18,42 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const { shop, topic, payload, session, admin } = await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`);
// Idempotency against Shopify retries — see webhooks/dedupe.server.ts.
if (await isDuplicateWebhook(request, shop, topic)) return new Response();
if (!session || !admin) {
// App was uninstalled before the webhook drained — nothing to do.
return new Response();
}
const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } });
if (!settings?.autoEmailOnFulfilledNonWireTransfer) return new Response();
const orderId = payload?.id;
if (orderId == null) return new Response();
const orderGid = `gid://shopify/Order/${String(orderId).replace(/^.*\//, "")}`;
if (await isManualPaymentOrder(admin, orderGid)) {
// Manual / wire-transfer order — handled by Automation 1, skip here.
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;
}
try {
const result = await generateAndEmailInvoice({
shopDomain: shop,
admin,
orderId,
customerLocale: typeof payload?.customer_locale === "string" ? payload.customer_locale : undefined,
customerLocale,
});
if (!result.ok) {
console.warn(`auto-email (fulfilled) failed for order ${orderId} on ${shop}: ${result.reason}`);
}
} catch (err) {
console.error(`auto-email (fulfilled) crashed for order ${orderId} on ${shop}:`, err);
}
});
return new Response();
};