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:
@@ -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/create — Automation 1: when a wire-transfer (manual-payment-gateway)
|
||||
@@ -16,31 +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}`);
|
||||
|
||||
if (!session || !admin) return new Response();
|
||||
// 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();
|
||||
|
||||
const settings = await db.shopSettings.findUnique({ where: { shopDomain: shop } });
|
||||
if (!settings?.autoEmailOnWireTransferPlaced) return new Response();
|
||||
if (!session || !admin) return new Response();
|
||||
|
||||
const orderId = payload?.id;
|
||||
if (orderId == null) return new Response();
|
||||
|
||||
const orderGid = `gid://shopify/Order/${String(orderId).replace(/^.*\//, "")}`;
|
||||
const isManual = await isManualPaymentOrder(admin, orderGid);
|
||||
if (!isManual) 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;
|
||||
|
||||
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 (wire-transfer placed) failed for order ${orderId} on ${shop}: ${result.reason}`);
|
||||
console.warn(
|
||||
`auto-email (wire-transfer placed) failed for order ${orderId} on ${shop}: ${result.reason}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`auto-email (wire-transfer placed) crashed for order ${orderId} on ${shop}:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
return new Response();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user