refactor(automations): detect manual payment via OrderTransaction.manualPaymentGateway

- 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.
This commit is contained in:
Gerhard Scheikl
2026-05-09 20:31:31 +02:00
parent 0800d1160b
commit 93aec2f368
6 changed files with 71 additions and 58 deletions
+49 -26
View File
@@ -4,32 +4,54 @@ import db from "../../db.server";
import { generateInvoice } from "./generateInvoice.server";
import { sendInvoiceEmail } from "./email.server";
const DEFAULT_WIRE_TRANSFER_NAMES = [
"manual",
"Überweisung",
"Wire Transfer",
"Bank Transfer",
"Vorkasse",
"Bank Deposit",
];
/**
* Returns true when any of the order's `payment_gateway_names` matches one of
* the configured wire-transfer gateway names (case-insensitive substring).
* Returns true when the order has at least one transaction processed by a
* Shopify "manual" payment gateway (wire transfer, cash on delivery,
* money order, custom manual methods, …). This uses
* `OrderTransaction.manualPaymentGateway`, the only first-class flag
* Shopify exposes for distinguishing manual gateways from automated ones.
*
* Falls back to `false` on any GraphQL error so we don't block fulfilment
* automations on transient API issues.
*/
export function isWireTransferOrder(
paymentGatewayNames: readonly string[] | null | undefined,
configured: string,
): boolean {
if (!paymentGatewayNames || paymentGatewayNames.length === 0) return false;
const needles = (configured.trim() ? configured.split(",") : DEFAULT_WIRE_TRANSFER_NAMES)
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
if (needles.length === 0) return false;
return paymentGatewayNames.some((name) => {
const n = (name ?? "").toLowerCase();
return needles.some((needle) => n.includes(needle));
});
export async function isManualPaymentOrder(
admin: AdminApiContext,
orderGid: string,
): Promise<boolean> {
try {
const res = await admin.graphql(
`#graphql
query OrderManualPaymentCheck($id: ID!) {
order(id: $id) {
transactions(first: 20) {
kind
status
manualPaymentGateway
}
}
}`,
{ variables: { id: orderGid } },
);
const json = (await res.json()) as {
data?: {
order?: {
transactions?: Array<{
kind?: string;
status?: string;
manualPaymentGateway?: boolean;
}>;
} | null;
};
};
const txs = json.data?.order?.transactions ?? [];
// Any non-failed transaction processed by a manual gateway counts.
return txs.some(
(t) => t.manualPaymentGateway === true && t.status !== "FAILURE" && t.status !== "ERROR",
);
} catch (err) {
console.warn(`isManualPaymentOrder query failed for ${orderGid}:`, err);
return false;
}
}
export interface AutoEmailArgs {
@@ -51,7 +73,8 @@ export async function generateAndEmailInvoice(args: AutoEmailArgs): Promise<{
reason?: string;
invoiceNumber?: string;
}> {
const orderGid = `gid://shopify/Order/${String(args.orderId).replace(/^.*\//, "")}`;
const orderNumeric = String(args.orderId).replace(/^.*\//, "");
const orderGid = `gid://shopify/Order/${orderNumeric}`;
const existing = await db.invoice.findFirst({
where: {
@@ -72,7 +95,7 @@ export async function generateAndEmailInvoice(args: AutoEmailArgs): Promise<{
const generated = await generateInvoice({
shopDomain: args.shopDomain,
admin: args.admin,
orderId: String(args.orderId),
orderId: orderNumeric,
});
invoiceId = generated.invoiceId;
invoiceNumber = generated.invoiceNumber;