dde53319e5
- 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.
60 lines
2.2 KiB
TypeScript
60 lines
2.2 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/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}`);
|
|
|
|
// 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 orderId = payload?.id;
|
|
if (orderId == null) 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) {
|
|
console.warn(`auto-email (fulfilled) failed for order ${orderId} on ${shop}: ${result.reason}`);
|
|
}
|
|
});
|
|
|
|
return new Response();
|
|
};
|