55a0dd03f2
- i18n.de: switch Sie/Ihren to du/dein for salutation, thank-you line, customer-VAT label and payment-terms paragraph. Closing line was already informal. - i18n: add paymentMethodLabel/paymentStatusLabel + per-status labels (paid/unpaid/partial/refunded) for both DE and EN, plus derivePaymentStatus helper that condenses Shopify's displayFinancialStatus (PAID, PARTIALLY_PAID, REFUNDED, …) into a 4-value enum. - loadOrderForInvoice: query Order.paymentGatewayNames and propagate it on the raw view-model. - composeInvoice + types: expose paymentStatus + paymentGatewayNames on InvoiceViewModel (filtered/trimmed). loadDraftOrderForOffer keeps paymentGatewayNames empty (drafts have no gateway yet). - InvoiceDocument: render two new meta rows on real invoices — 'Zahlart / Payment method' (joined, prettified gateway names) and 'Zahlstatus / Payment status' (translated label). Storno + offer kinds intentionally omit them. - scripts/render-sample.ts: extend smoke checks to assert the informal DE wording, the new payment-method/status rows and the paymentStatus/paymentGatewayNames composer outputs.
186 lines
5.0 KiB
TypeScript
186 lines
5.0 KiB
TypeScript
import type { AdminApiContext } from "@shopify/shopify-app-react-router/server";
|
|
|
|
import type {
|
|
RawAddress,
|
|
RawLineItem,
|
|
RawMoney,
|
|
RawOrderForInvoice,
|
|
RawTaxLine,
|
|
} from "./loadOrderForInvoice.server";
|
|
|
|
/**
|
|
* Loads a Shopify DraftOrder and adapts it to the same `RawOrderForInvoice`
|
|
* shape used for completed orders, so the rest of the pipeline (composer,
|
|
* PDF, etc.) doesn't need to know whether it's rendering an invoice or an
|
|
* offer.
|
|
*
|
|
* Drafts have no `processedAt` (we use createdAt) and no
|
|
* `displayFinancialStatus` (we treat them as not paid).
|
|
*/
|
|
const QUERY = `#graphql
|
|
query DraftOrderForOffer($id: ID!) {
|
|
draftOrder(id: $id) {
|
|
id
|
|
name
|
|
createdAt
|
|
currencyCode
|
|
taxesIncluded
|
|
customer {
|
|
firstName
|
|
lastName
|
|
email
|
|
locale
|
|
}
|
|
billingAddress {
|
|
name
|
|
company
|
|
address1
|
|
address2
|
|
zip
|
|
city
|
|
province
|
|
countryCode: countryCodeV2
|
|
}
|
|
shippingAddress {
|
|
name
|
|
company
|
|
address1
|
|
address2
|
|
zip
|
|
city
|
|
province
|
|
countryCode: countryCodeV2
|
|
}
|
|
subtotalPriceSet { shopMoney { amount currencyCode } }
|
|
totalTaxSet { shopMoney { amount currencyCode } }
|
|
totalPriceSet { shopMoney { amount currencyCode } }
|
|
taxLines {
|
|
title
|
|
rate
|
|
ratePercentage
|
|
priceSet { shopMoney { amount currencyCode } }
|
|
}
|
|
lineItems(first: 250) {
|
|
edges {
|
|
node {
|
|
title
|
|
sku
|
|
quantity
|
|
originalUnitPriceSet { shopMoney { amount currencyCode } }
|
|
image { url altText }
|
|
taxLines {
|
|
title
|
|
rate
|
|
ratePercentage
|
|
priceSet { shopMoney { amount currencyCode } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
purchasingEntity {
|
|
... on PurchasingCompany {
|
|
company { name }
|
|
location {
|
|
taxRegistrationId
|
|
billingAddress {
|
|
address1
|
|
address2
|
|
zip
|
|
city
|
|
countryCode
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
interface RawAdminResponse {
|
|
data?: {
|
|
draftOrder?: {
|
|
id: string;
|
|
name: string;
|
|
createdAt: string;
|
|
currencyCode: string;
|
|
taxesIncluded: boolean;
|
|
customer: {
|
|
firstName: string | null;
|
|
lastName: string | null;
|
|
email: string | null;
|
|
locale: string | null;
|
|
} | null;
|
|
billingAddress: RawAddress | null;
|
|
shippingAddress: RawAddress | null;
|
|
subtotalPriceSet: { shopMoney: RawMoney } | null;
|
|
totalTaxSet: { shopMoney: RawMoney } | null;
|
|
totalPriceSet: { shopMoney: RawMoney } | null;
|
|
taxLines: RawTaxLine[];
|
|
lineItems: { edges: { node: RawLineItem & { image?: { url: string | null } | null } }[] };
|
|
purchasingEntity: {
|
|
company?: { name: string } | null;
|
|
location?: {
|
|
taxRegistrationId: string | null;
|
|
billingAddress: RawAddress | null;
|
|
} | null;
|
|
} | null;
|
|
} | null;
|
|
};
|
|
}
|
|
|
|
export async function loadDraftOrderForOffer(
|
|
admin: AdminApiContext,
|
|
draftOrderGid: string,
|
|
): Promise<RawOrderForInvoice> {
|
|
const response = await admin.graphql(QUERY, { variables: { id: draftOrderGid } });
|
|
const json = (await response.json()) as RawAdminResponse;
|
|
const draft = json.data?.draftOrder;
|
|
if (!draft) {
|
|
throw new Error(`Draft order ${draftOrderGid} not found.`);
|
|
}
|
|
|
|
const purchasingCompany = draft.purchasingEntity?.company
|
|
? {
|
|
name: draft.purchasingEntity.company.name,
|
|
vatId: draft.purchasingEntity.location?.taxRegistrationId ?? null,
|
|
address: draft.purchasingEntity.location?.billingAddress ?? null,
|
|
}
|
|
: null;
|
|
|
|
// Drafts don't have a numeric "order number" — use a hash of the GID as a
|
|
// numeric proxy for the invoice-counter signature (not actually used when
|
|
// generating offers, but kept non-zero to satisfy downstream code).
|
|
const orderNumber = parseInt(draft.id.replace(/[^0-9]/g, "").slice(-9), 10) || 0;
|
|
|
|
return {
|
|
id: draft.id,
|
|
name: draft.name,
|
|
orderNumber,
|
|
createdAt: draft.createdAt,
|
|
processedAt: null,
|
|
currencyCode: draft.currencyCode,
|
|
displayFinancialStatus: null,
|
|
paymentGatewayNames: [],
|
|
taxesIncluded: draft.taxesIncluded,
|
|
customer: draft.customer,
|
|
billingAddress: draft.billingAddress,
|
|
shippingAddress: draft.shippingAddress,
|
|
subtotalSet: draft.subtotalPriceSet,
|
|
totalTaxSet: draft.totalTaxSet,
|
|
totalPriceSet: draft.totalPriceSet,
|
|
taxLines: draft.taxLines || [],
|
|
lineItems: (draft.lineItems?.edges || []).map((e) => {
|
|
const node = e.node;
|
|
return {
|
|
title: node.title,
|
|
sku: node.sku,
|
|
quantity: node.quantity,
|
|
originalUnitPriceSet: node.originalUnitPriceSet,
|
|
taxLines: node.taxLines,
|
|
imageUrl: node.image?.url ?? null,
|
|
};
|
|
}),
|
|
purchasingEntity: { company: purchasingCompany },
|
|
};
|
|
}
|