diff --git a/astro.sidebar.mjs b/astro.sidebar.mjs index 58ba34c95..89a52f48d 100644 --- a/astro.sidebar.mjs +++ b/astro.sidebar.mjs @@ -728,10 +728,12 @@ export function generateSidebar() { { label: 'Overview', link: '/dropins/order/containers/' }, { label: 'CreateReturn', link: '/dropins/order/containers/create-return/' }, { label: 'CustomerDetails', link: '/dropins/order/containers/customer-details/' }, + { label: 'InvoiceDetails', link: '/dropins/order/containers/invoice-details/' }, { label: 'OrderCancelForm', link: '/dropins/order/containers/order-cancel-form/' }, { label: 'OrderComments', link: '/dropins/order/containers/order-comments/' }, { label: 'OrderCostSummary', link: '/dropins/order/containers/order-cost-summary/' }, { label: 'OrderHeader', link: '/dropins/order/containers/order-header/' }, + { label: 'OrderInvoices', link: '/dropins/order/containers/order-invoices/' }, { label: 'OrderProductList', link: '/dropins/order/containers/order-product-list/' }, { label: 'OrderReturns', link: '/dropins/order/containers/order-returns/' }, { label: 'OrderSearch', link: '/dropins/order/containers/order-search/' }, diff --git a/public/images/dropins/order/invoice-details.png b/public/images/dropins/order/invoice-details.png new file mode 100644 index 000000000..40d7fc462 Binary files /dev/null and b/public/images/dropins/order/invoice-details.png differ diff --git a/public/images/dropins/order/order-invoices.png b/public/images/dropins/order/order-invoices.png new file mode 100644 index 000000000..47baa1a1b Binary files /dev/null and b/public/images/dropins/order/order-invoices.png differ diff --git a/src/content/docs/dropins/order/containers/index.mdx b/src/content/docs/dropins/order/containers/index.mdx index 2476f3772..db6d79744 100644 --- a/src/content/docs/dropins/order/containers/index.mdx +++ b/src/content/docs/dropins/order/containers/index.mdx @@ -23,10 +23,12 @@ Containers are pre-built UI components that combine functionality, state managem | --------- | ----------- | | [CreateReturn](/dropins/order/containers/create-return/) | Learn about the `CreateReturn` container. | | [CustomerDetails](/dropins/order/containers/customer-details/) | Learn about the `CustomerDetails` container. | +| [InvoiceDetails](/dropins/order/containers/invoice-details/) | Display one invoice within an order and download a PDF. | | [OrderCancelForm](/dropins/order/containers/order-cancel-form/) | Learn about the `OrderCancelForm` container. | | [OrderComments](/dropins/order/containers/order-comments/) | Learn about the `OrderComments` container. | | [OrderCostSummary](/dropins/order/containers/order-cost-summary/) | Learn about the `OrderCostSummary` container. | | [OrderHeader](/dropins/order/containers/order-header/) | *Enrichment needed - add description to `_dropin-enrichments/order/containers.json`* | +| [OrderInvoices](/dropins/order/containers/order-invoices/) | List invoices associated with an authenticated customer's order. | | [OrderProductList](/dropins/order/containers/order-product-list/) | Learn about the `OrderProductList` container. | | [OrderReturns](/dropins/order/containers/order-returns/) | Learn about the `OrderReturns` container. | | [OrderSearch](/dropins/order/containers/order-search/) | Learn about the `OrderSearch` container. | diff --git a/src/content/docs/dropins/order/containers/invoice-details.mdx b/src/content/docs/dropins/order/containers/invoice-details.mdx new file mode 100644 index 000000000..6e2ac08f2 --- /dev/null +++ b/src/content/docs/dropins/order/containers/invoice-details.mdx @@ -0,0 +1,154 @@ +--- +title: InvoiceDetails container +description: Display an invoice for a customer's order and download a PDF with optional host-provided branding. +--- + +import OptionsTable from '@components/OptionsTable.astro'; +import Aside from '@components/Aside.astro'; +import Diagram from '@components/Diagram.astro'; + +The `InvoiceDetails` container displays one invoice associated with an order. It shows the invoice and order numbers, order date, payment method, billing and shipping addresses, products, invoiced quantities, and invoice totals. + + + ![An invoice details view with addresses, invoiced products, totals, and a Download PDF + button.](@images/dropins/order/invoice-details.png) + + + + +## Configurations + +The `InvoiceDetails` container provides the following configuration options: + +
+ string', + 'No', + 'Returns the destination for the Back to Order Details link. Use your order details or invoice-list route.', + ], + [ + 'onDownloadPdf', + '() => void', + 'No', + 'Replaces the built-in PDF download. Omit this callback to use browser-generated PDFs.', + ], + [ + 'getInvoicePdfBranding', + '() => InvoicePdfBranding\n| undefined\n| Promise', + 'No', + 'Supplies branding to the built-in PDF generator. This callback is not used when onDownloadPdf replaces the download.', + ], + ]} + /> +
+ + + +## Order context and states + +Provide `orderData` or receive an [`order/data`](/dropins/order/events/#orderdata-emits-and-listens) event from the existing Order initializer. The container fetches invoices for that order and matches `invoiceRef` against the invoice number or ID. + +The container shows a loader during retrieval, a not-found alert if the invoice is absent from the order, and a separate error alert if retrieval fails. + +## Example + +This example reads both references from the URL, loads the order, and uses the built-in PDF download. The host application must authenticate the customer before fetching the order. Replace the paths with your storefront routes. + +```javascript +import '../../scripts/initializers/order.js'; +import { getOrderDetailsById } from '@dropins/storefront-order/api.js'; +import { InvoiceDetails } from '@dropins/storefront-order/containers/InvoiceDetails.js'; +import { render as orderRenderer } from '@dropins/storefront-order/render.js'; + +export default async function decorate(block) { + const { searchParams } = new URL(window.location.href); + const orderRef = searchParams.get('orderRef'); + const invoiceRef = searchParams.get('invoiceRef'); + if (!orderRef || !invoiceRef) { + throw new Error('Order and invoice references are required.'); + } + + const orderData = await getOrderDetailsById({ + orderId: orderRef, + queryType: 'orderData', + }); + + await orderRenderer.render(InvoiceDetails, { + orderData, + invoiceRef, + routeInvoicesList: () => { + const query = new URLSearchParams({ orderRef }); + return `/order-details?${query}`; + }, + })(block); +} +``` + +## PDF download and branding + +The built-in download creates a portrait A4 PDF in the browser and loads the renderer when the customer requests it. The filename is `invoice-.pdf`, with unsupported filename characters replaced by hyphens. The container does not store PDFs on the server. + +To add branding, supply `getInvoicePdfBranding` in the render options. Return an object with any of these fields: + + + +Add the callback to the render options in the previous example: + +```javascript +await orderRenderer.render(InvoiceDetails, { + orderData, + invoiceRef, + getInvoicePdfBranding: () => ({ + storeName: 'Example Store', + addressLines: ['100 Main Street', 'Austin, TX 78701'], + }), +})(block); +``` + +This branding is supplied by the host application. The renderer does not fetch the PDF logo or address configured in Adobe Commerce Admin. If your host loads an image from a different origin to create a data URL, that origin must allow the image request. + +To use a backend document service instead, supply `onDownloadPdf` and implement the download in that callback. Do not pass an empty callback: it disables the built-in download. + +## Current data limitations + +- The current invoice query does not retrieve invoice status. +- The detail view and PDF calculate each line subtotal as sale price multiplied by invoiced quantity. The invoice's aggregate subtotal and grand total come from the backend; computed line amounts can differ from stored row totals for partial invoices because of rounding. +- Discounts appear separately in invoice totals. Item-level discount data is available in the model, but is not displayed as an additional line discount column. +- The PDF does not estimate item tax from order prices. It shows zero in the Tax column when aggregate invoice tax is zero, and an unavailable marker when aggregate tax is nonzero but item tax is unavailable. Aggregate tax appears in totals. +- The UI supports the [Order dictionary](/dropins/order/dictionary/). Built-in PDF headings use fixed English labels, and the default download uses the renderer's `en-US` locale. + +For a list of invoices within the order, see [`OrderInvoices`](/dropins/order/containers/order-invoices/). diff --git a/src/content/docs/dropins/order/containers/order-invoices.mdx b/src/content/docs/dropins/order/containers/order-invoices.mdx new file mode 100644 index 000000000..6cacadc6a --- /dev/null +++ b/src/content/docs/dropins/order/containers/order-invoices.mdx @@ -0,0 +1,86 @@ +--- +title: OrderInvoices container +description: Display invoices for a customer's order and link each invoice to its details page. +--- + +import OptionsTable from '@components/OptionsTable.astro'; +import Aside from '@components/Aside.astro'; +import Diagram from '@components/Diagram.astro'; + +The `OrderInvoices` container displays invoices associated with one order. Each entry shows the invoice number, total amount, and invoiced item count. The item count is the sum of invoiced quantities, not the number of product lines. + + + ![An order invoice list showing an invoice number, total amount, and invoiced item + count.](@images/dropins/order/order-invoices.png) + + + + +## Configurations + +The `OrderInvoices` container provides the following configuration options: + + string', + 'No', + 'Returns the URL for an invoice details page. Receives the selected invoice and its order number.', + ], + ]} +/> + +## Order context and states + +Provide `orderData` or load the order through the existing Order initializer so that the container receives an [`order/data`](/dropins/order/events/#orderdata-emits-and-listens) event. Passing an order does not bypass customer authorization. + +The container shows a loader while the order or its invoices load. It shows an empty state when the order has no invoices and a separate error alert when invoice retrieval fails. + +## Example + +This example uses the order number in the page's `orderRef` query parameter. The host application must initialize the Order drop-in and authenticate the customer before fetching the order. Replace the example paths with your storefront routes. + +```javascript +import '../../scripts/initializers/order.js'; +import { getOrderDetailsById } from '@dropins/storefront-order/api.js'; +import { OrderInvoices } from '@dropins/storefront-order/containers/OrderInvoices.js'; +import { render as orderRenderer } from '@dropins/storefront-order/render.js'; + +export default async function decorate(block) { + const { searchParams } = new URL(window.location.href); + const orderRef = searchParams.get('orderRef'); + if (!orderRef) throw new Error('An order reference is required.'); + + const orderData = await getOrderDetailsById({ + orderId: orderRef, + queryType: 'orderData', + }); + + await orderRenderer.render(OrderInvoices, { + orderData, + routeInvoiceDetails: (invoice, orderNumber) => { + const query = new URLSearchParams({ + orderRef: orderNumber, + invoiceRef: invoice.number, + }); + return `/invoice-details?${query}`; + }, + })(block); +} +``` + +Use [`InvoiceDetails`](/dropins/order/containers/invoice-details/) on the destination page. For programmatic invoice retrieval, see [`getOrderInvoices`](/dropins/order/functions/#getorderinvoices). diff --git a/src/content/docs/dropins/order/dictionary.mdx b/src/content/docs/dropins/order/dictionary.mdx index ca3b6e93a..55ae50fcf 100644 --- a/src/content/docs/dropins/order/dictionary.mdx +++ b/src/content/docs/dropins/order/dictionary.mdx @@ -41,11 +41,78 @@ You only need to include the keys you want to change. For multi-language support ## Default keys and values +### Invoice labels + +Invoice UI strings use `Order.OrderInvoices` and `Order.InvoiceDetails`. Customize list headings, item counts, navigation labels, download-button text, column labels, totals, and error messages through the existing `langDefinitions` configuration. + +```javascript +const invoiceLabels = { + Order: { + OrderInvoices: { + title: 'Order invoices', + emptyState: 'No invoices for this order.', + }, + InvoiceDetails: { + backToOrderDetails: 'Back to your order', + downloadPdf: 'Download invoice', + }, + }, +}; +``` + +Merge these overrides into the locale entry in `langDefinitions`. They change the storefront interface, not the fixed English headings inside the built-in PDF. See [PDF download and branding](/dropins/order/containers/invoice-details/#pdf-download-and-branding). + Below are the default English (`en_US`) strings provided by the **Order** drop-in: ```json title="en_US.json" { "Order": { + "OrderInvoices": { + "emptyState": "No invoices.", + "title": "Invoices", + "invoiceNumber": "Invoice #", + "itemsCount": { + "singular": "{{count}} item", + "plural": "{{count}} items" + }, + "loadError": { + "heading": "Unable to load invoices", + "description": "Please try again later." + } + }, + "InvoiceDetails": { + "backToOrderDetails": "Back to Order Details", + "downloadPdf": "Download PDF", + "invoiceNumberLabel": "Invoice number", + "orderNumber": "Order number", + "orderDate": "Order date", + "paymentMethod": "Payment method", + "status": "Status", + "billingAddress": "Billing address", + "shippingAddress": "Shipping address", + "items": { + "productName": "Product Name", + "sku": "SKU", + "price": "Unit price", + "qtyInvoiced": "Qty Invoiced", + "subtotal": "Subtotal" + }, + "totals": { + "subtotal": "Subtotal", + "discounts": "Discounts", + "tax": "Tax", + "shipping": "Shipping & Handling", + "grandTotal": "Total" + }, + "notFound": { + "heading": "Invoice not found", + "description": "We couldn't find this invoice, or it doesn't belong to your account." + }, + "loadError": { + "heading": "Unable to load invoice", + "description": "Please try again later." + } + }, "CreateReturn": { "headerText": "Return items", "downloadableCount": "Files", diff --git a/src/content/docs/dropins/order/events.mdx b/src/content/docs/dropins/order/events.mdx index 982466d7a..c1f12b9b5 100644 --- a/src/content/docs/dropins/order/events.mdx +++ b/src/content/docs/dropins/order/events.mdx @@ -98,6 +98,8 @@ events.on('companyContext/changed', (payload) => { Emitted when order data is loaded or updated. This includes order details, items, shipping information, and payment status. +[OrderInvoices](/dropins/order/containers/order-invoices/) and [InvoiceDetails](/dropins/order/containers/invoice-details/) listen eagerly to this event, so they can use the most recent order payload when mounted. They fetch invoices using the order number and refetch when it changes. `InvoiceDetails` also refetches when its invoice reference changes. Fetching invoices does not emit a separate invoice event or add the invoice response to the `order/data` payload. + #### Event payload ```typescript diff --git a/src/content/docs/dropins/order/functions.mdx b/src/content/docs/dropins/order/functions.mdx index 3188f173e..4786da3a4 100644 --- a/src/content/docs/dropins/order/functions.mdx +++ b/src/content/docs/dropins/order/functions.mdx @@ -28,6 +28,7 @@ The Order drop-in provides API functions that enable you to programmatically con | [`getCustomerOrdersReturn`](#getcustomerordersreturn) | Returns details about the returns a customer has requested. | | [`getGuestOrder`](#getguestorder) | Is a wrapper for the `guestOrder` query. | | [`getOrderDetailsById`](#getorderdetailsbyid) | Fetches detailed order data by order ID from the Commerce backend. | +| [`getOrderInvoices`](#getorderinvoices) | Retrieves invoices for an authenticated customer's order by order number. | | [`getStoreConfig`](#getstoreconfig) | Returns information about the storefront configuration. | | [`guestOrderByToken`](#guestorderbytoken) | Retrieves a guest order using a token generated by Adobe Commerce. | | [`placeNegotiableQuoteOrder`](#placenegotiablequoteorder) | Places an order for a negotiable quote. | @@ -252,6 +253,45 @@ Does not emit any drop-in events. Returns [`OrderDataModel`](#orderdatamodel) or `null`. +## getOrderInvoices + +The `getOrderInvoices` function retrieves invoices for one order through the authenticated customer's `customer.orders` query. Pass the order number, not a guest order token. Use a package release that exports this function. + +```typescript +declare function getOrderInvoices(orderNumber: string): Promise; +``` + +```javascript +import { getOrderInvoices } from '@dropins/storefront-order/api.js'; + +try { + const result = await getOrderInvoices('000000114'); + const invoices = result?.invoices ?? []; + console.log(invoices); +} catch (error) { + console.error('Unable to load order invoices.', error); +} +``` + +### Events + +Does not emit any drop-in events. [OrderInvoices](/dropins/order/containers/order-invoices/) and [InvoiceDetails](/dropins/order/containers/invoice-details/) call this function after receiving their order context. + +### Returns + +Returns a normalized `OrderInvoicesModel`, or `null` when the response contains no matching order. A matching order without invoices returns an empty `invoices` array. Request failures reject the promise. + +```typescript +interface OrderInvoicesModel { + orderNumber: string; + invoices: OrderInvoiceModel[]; +} +``` + +Each invoice contains its `id`, `number`, `comments`, `items`, and `total`. Items include `productSku`, `productName`, `quantityInvoiced`, `productSalePrice`, and optional `discounts`. Totals include `subtotal`, `grandTotal`, `baseGrandTotal`, `taxes`, optional `shippingHandling`, and `discounts`. Money values contain `value` and `currency`. + +The current query does not retrieve invoice status, invoice-item tax amounts, or stored invoice-item row totals. See the [invoice data limitations](/dropins/order/containers/invoice-details/#current-data-limitations) before calculating line amounts. + ## getStoreConfig The `getStoreConfig` function returns information about the storefront configuration. It is a wrapper for the query. diff --git a/src/content/docs/dropins/order/index.mdx b/src/content/docs/dropins/order/index.mdx index 6ef7051d9..60c202108 100644 --- a/src/content/docs/dropins/order/index.mdx +++ b/src/content/docs/dropins/order/index.mdx @@ -19,6 +19,7 @@ The order drop-in component consists of multiple containers that display order d - Order data containers display order details within the customer account, guest user areas, and on the order confirmation page. - Returned merchandise authorization (RMA) containers guide users through the return process and display a list of created return requests. +- Invoice containers let authenticated customers view invoices within an order. [OrderInvoices](/dropins/order/containers/order-invoices/) lists those invoices, and [InvoiceDetails](/dropins/order/containers/invoice-details/) displays one invoice and provides a PDF download. These containers do not provide cross-order invoice management or guest invoice retrieval. - The `OrderSearch` container enables guest users to locate their orders using a combination of email, last name, and order number. This ensures easy access to order details even for users without an account. (Logged-in customers can also use this form.) The component's initialization process helps manage data retrieval and event emission, ensuring that containers receive the necessary data without individual fetching. diff --git a/src/content/docs/dropins/order/initialization.mdx b/src/content/docs/dropins/order/initialization.mdx index 0edf8d2ec..1399c49b0 100644 --- a/src/content/docs/dropins/order/initialization.mdx +++ b/src/content/docs/dropins/order/initialization.mdx @@ -87,6 +87,7 @@ The following models can be customized through the `models` configuration option | Model | Description | |---|---| | [`OrderDataModel`](#orderdatamodel) | Transforms order data from `GraphQL` including order details, items, shipping, billing, payment, and tracking information. Use this to add custom fields specific to your order management workflow. | +| [`OrderInvoicesModel`](#orderinvoicesmodel) | Transforms invoices returned for an order by `getOrderInvoices`. | | [`CustomerOrdersReturnModel`](#customerordersreturnmodel) | Transforms `CustomerOrdersReturnModel` data from `GraphQL`. | | [`RequestReturnModel`](#requestreturnmodel) | Transforms `RequestReturnModel` data from `GraphQL`. | @@ -113,6 +114,14 @@ const models = { await initializers.mountImmediately(initialize, { models }); ``` +### OrderInvoicesModel + +The `models.OrderInvoicesModel.transformer` callback receives the raw order entry returned by the invoice query. Its result is merged with the default normalized model. Use this extension point to add fields already present in the response; a transformer cannot retrieve fields that the GraphQL query does not select. + +Add this transformer to the `models` options supplied by your existing Order initializer. Preserve the normalized invoice fields when overriding them so that the invoice containers can still render the response. + +For the normalized result, see [`getOrderInvoices`](/dropins/order/functions/#getorderinvoices). Invoice containers use the existing Order initialization and customer authentication; they do not introduce a separate initializer. + ## Drop-in configuration The **Order initializer** configures how order data is managed and displayed, including order history, status tracking, and order details. Use initialization to customize order data structures and integrate order management features. diff --git a/src/content/docs/dropins/order/quick-start.mdx b/src/content/docs/dropins/order/quick-start.mdx index 1b62a1d13..1c85a6899 100644 --- a/src/content/docs/dropins/order/quick-start.mdx +++ b/src/content/docs/dropins/order/quick-start.mdx @@ -51,6 +51,8 @@ export default async function decorate(block) { ## Learn more +For an authenticated customer's order invoices, use [OrderInvoices](/dropins/order/containers/order-invoices/) and [InvoiceDetails](/dropins/order/containers/invoice-details/). Keep both the order and invoice references in your navigation URLs. These containers use the existing Order initializer; no separate Invoice drop-in initialization is required. Confirm that your package release includes the invoice containers and PDF options before integrating them. + - [Containers](/dropins/order/containers/) - Available UI components and configuration options - [Initialization](/dropins/order/initialization/) - Customize initializer settings and data models - [Functions](/dropins/order/functions/) - Control drop-in behavior programmatically diff --git a/src/content/docs/dropins/order/slots.mdx b/src/content/docs/dropins/order/slots.mdx index 86e9c94b3..9630500b7 100644 --- a/src/content/docs/dropins/order/slots.mdx +++ b/src/content/docs/dropins/order/slots.mdx @@ -14,6 +14,8 @@ import { Aside } from '@astrojs/starlight/components'; The Order drop-in exposes slots for customizing specific UI sections. Use slots to replace or extend container components. For default properties available to all slots, see [Extending drop-in components](/dropins/all/extending/). +The current [OrderInvoices](/dropins/order/containers/order-invoices/) and [InvoiceDetails](/dropins/order/containers/invoice-details/) containers do not expose invoice-specific slots. Use their documented configuration options, dictionary keys, and CSS classes instead. + | Container | Slots | diff --git a/src/content/docs/dropins/order/styles.mdx b/src/content/docs/dropins/order/styles.mdx index 6cf144b2d..d0df7f5e3 100644 --- a/src/content/docs/dropins/order/styles.mdx +++ b/src/content/docs/dropins/order/styles.mdx @@ -26,6 +26,8 @@ For a complete list of available design tokens (colors, spacing, typography, and The Order drop-in uses BEM-style class naming. Use the browser DevTools to inspect elements and find specific class names. +Invoice containers use `.order-order-invoices-container` and `.order-invoice-details-container` as wrapper classes. The list uses `.order-order-invoices__item` and `.order-order-invoices__number`; the details view uses `.invoice-details-content__items-table` and `.invoice-details-content__totals`. These classes affect the storefront UI only. Browser-generated PDFs use a separate drawing layout and do not inherit page CSS. + ```css /* CustomerDetailsContent */ .dropin-card__content {}