The previous single body field could not distinguish 'use the default' from
'save the pre-filled default', so the first settings save locked the hint in
as a custom override for ALL orders — the paid/unpaid default logic never ran.
Give each payment state its own editable body field (emailBodyHtmlDePaid /
emailBodyHtmlEnPaid) with its own empty-fallback. A body submitted unchanged
from its built-in default is stored as empty, so 'use default' survives saves
and future default updates propagate.
Persist paymentStatus on the Invoice at generation time and use it at send
time to pick the default email body. Paid orders (paid/refunded/voided) now
get a body without the bank-transfer reference hint; outstanding orders keep
it. Custom merchant templates still override the defaults for both cases.
- Add Invoice.paymentStatus column + migration
- Split DEFAULT_EMAIL_BODY_{DE,EN} into _PAID/_UNPAID variants
- Persist paymentStatus in generateInvoice; select default in email.server
- Document behaviour on the settings page
- Add email-templates test (paid omits hint, unpaid keeps it)
Two related fixes around the order/invoice number:
1) The thank-you page and the customer-account order page were showing
the bare Shopify order name (e.g. '#1034') as the payment reference,
while the PDF (and its GiroCode QR) used the canonical invoice
number (e.g. 'RE-1034'). Banks treat each unique reference as a
separate payment, and several reject the '#' character outright \u2014
so customers who pasted the thank-you reference into their banking
app ended up with a payment the shop couldn't reconcile.
New shared helper resolveOrderRemittance() (services/invoice/
remittance.server.ts) returns the single source of truth for the
reference: latest non-cancelled Invoice row for the order, falling
back to '${prefix}${orderNumber}' when no PDF has been generated yet.
Both /api/public/payment-info and /api/public/girocode.png now route
through it, so the thank-you page, the customer-account page and the
GiroCode QR are guaranteed to match the PDF byte-for-byte.
2) Drop the redundant '\u00b7 Bestellnummer: #1004' suffix from the PDF
title when the invoice number's trailing digits already match the
Shopify order name (default 'order_number' numbering mode). In that
mode the two strings carry identical numeric content and the suffix
only adds noise; sequential mode (RE-7 vs #1004) keeps the suffix.
- New smoke assertion verifies the suppression triggers on
invoiceNumber='RE-1004' + orderName='#1004' and that the invoice
number itself is still shown.
- Both endpoints now also query 'Order.number' (already covered by
read_orders) so the fallback path can build the prefix+order-number
string without requiring the Invoice row.
- 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.
- Drop wireTransferGatewayNames from ShopSettings (new migration).
- Replace string-matching with a GraphQL query against
Order.transactions[].manualPaymentGateway, the first-class flag
Shopify exposes for any merchant-defined manual payment method.
- Both webhook handlers now fetch the order on the fly to classify it,
removing the configurable gateway-names field from settings.
- New ShopSettings fields: autoEmailOnWireTransferPlaced,
autoEmailOnFulfilledNonWireTransfer, wireTransferGatewayNames.
- New Automations section in settings with two toggles + gateway list.
- orders/create webhook now fires automation 1 (wire-transfer placed).
- New orders/fulfilled webhook fires automation 2 (non-wire-transfer fulfilled).
- Shared helper services/invoice/automations.server.ts handles classification
and idempotent generate+send (skips if already sent).
- Webhook subscription for orders/fulfilled added to all 3 app tomls.
This is the non-Plus fallback for Shopify Flow, whose custom-app actions
are gated to Plus stores only.
Mirrors the layout from data/mail_template.png:
- Company name + greeting headline
- Body referencing the invoice number
- Inline logo (cid:invoice-logo) attached automatically
- Footer with mailto + website links
New template vars: {{shopEmail}}, {{shopWebsite}}.
Settings UI prefills empty fields with the defaults so users see and
can tweak them without losing the fallback.
Adds a Send button to the order action extension and a corresponding
"send" op to /api/orders/:orderId/invoice. Generates the invoice on
demand if missing, then sends via the configured SMTP.
The order-action / order-block UI extensions are hosted on
extensions.shopifycdn.com and call our app via fetch(). Without CORS
headers the browser blocked the response. authenticate.admin already
returns a cors helper and handles OPTIONS preflight - wrap every
Response with it.
Vite/React Router refused to bundle the client because
app/routes/app.settings.tsx imported the constant from a .server file
and used it inside the route component (not just loader/action), so it
could not be tree-shaken out.
Move the sentinel to logoCache.constants.ts, re-export from
logoCache.server.ts for backwards compatibility, and import the constant
from constants in the route while keeping the server-only functions
(deleteStoredLogo, storeUploadedLogo) imported from .server (they are
only referenced inside the action and get tree-shaken correctly).