import type { AdminApiContext } from "@shopify/shopify-app-react-router/server"; import db from "../../db.server"; import { generateInvoice } from "./generateInvoice.server"; import { sendInvoiceEmail } from "./email.server"; /** * 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 async function isManualPaymentOrder( admin: AdminApiContext, orderGid: string, ): Promise { 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 { shopDomain: string; admin: AdminApiContext; /** Numeric Shopify order id (from REST webhook payload `id`). */ orderId: string | number; /** Customer locale forwarded to the email service for subject/body language. */ customerLocale?: string; } /** * Idempotent: if an unsent invoice already exists for the order, it is reused * and emailed. If a sent invoice already exists, sending is skipped (we never * spam the customer with the same invoice twice from automations). */ export async function generateAndEmailInvoice(args: AutoEmailArgs): Promise<{ ok: boolean; reason?: string; invoiceNumber?: string; }> { const orderNumeric = String(args.orderId).replace(/^.*\//, ""); const orderGid = `gid://shopify/Order/${orderNumeric}`; const existing = await db.invoice.findFirst({ where: { shopDomain: args.shopDomain, orderId: orderGid, kind: "invoice", cancelledAt: null, }, orderBy: [{ version: "desc" }, { createdAt: "desc" }], }); if (existing && existing.sentAt) { return { ok: true, reason: "already-sent", invoiceNumber: existing.invoiceNumber }; } let invoiceId = existing?.id; let invoiceNumber = existing?.invoiceNumber; if (!invoiceId) { const generated = await generateInvoice({ shopDomain: args.shopDomain, admin: args.admin, orderId: orderNumeric, }); invoiceId = generated.invoiceId; invoiceNumber = generated.invoiceNumber; } const result = await sendInvoiceEmail({ shopDomain: args.shopDomain, invoiceId, customerLocale: args.customerLocale, }); if (!result.ok) { return { ok: false, reason: result.errorMessage ?? "email-failed", invoiceNumber }; } return { ok: true, invoiceNumber }; }