## Summary Maps every code path in the API repo that creates an e-conomic draft invoice or sends draft lines, and documents which paths pick a layout, which one they pick, and how the planned **with-discounts / without-discounts** two-layout selection applies. **Key finding:** the two envelope creators already implement a discount-aware selector. No code change is required for the TRU-197 rollout — only the two `invoice*LayoutNumber` config variables need to be set in the `economic` module. ## Findings at a glance - **22** code paths in `services/nginx/app/` create or send draft invoices (2 envelope creators + 6 line-add paths + 14 caller/selector/helper paths) - **2** paths currently pick a layout — both already discount-aware - **0** paths need updating for the 2-layout rollout - **2** config variables drive the selection: `invoiceLayoutNumber` and `invoiceDiscountLayoutNumber` (already wired into `economic::$config` and the OpenAPI schema) ## The two selectors 1. `economic_invoice_draft_mo::resolveLayoutNumber()` at `services/nginx/app/modules/economic/invoices/draft/economic_invoice_draft_mo.php:115` — used by `createInvoiceDraftExample()` for the single-order draft flow. 2. `collected_order_invoices_o::resolveInvoiceLayoutNumber()` at `services/nginx/app/objects/collected_order_invoices_o.php:673` — used by `createInvoiceDraft()` for the collected-invoice flow. Both return `invoice_discount_layout` if any item has a non-zero discount, otherwise `invoice_layout`. They throw if the discount layout is required and `invoiceDiscountLayoutNumber` is unconfigured. ## Document `documentation/economic/layout-selection-flow.md` — full inventory table, current/desired state, and migration plan. ## Related - TRU-197 — `documentation/economic/invoice-template-audit.md` - TRU-193 — `documentation/economic/export-field-audit.md` - PR #391 — `economic_export_sanitizer` Refs: TRU-198 --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: OpenClaw <openclaw@copenhagentruckwash.io> Co-authored-by: TRU-198 Subagent <subagent@openhands.dev>
275 lines
21 KiB
Markdown
275 lines
21 KiB
Markdown
# E-conomic Draft-Invoice Layout-Selection Flow (TRU-198)
|
||
|
||
**Status:** Complete (investigation only — no code changes)
|
||
**Date:** 2026-08-17
|
||
**Scope:** Inventory every code path in `copenhagentruckwash/api` that creates
|
||
an e-conomic draft invoice or sends draft lines, and document whether each
|
||
path currently picks a layout, which one it picks, and how the planned
|
||
**with-discounts / without-discounts** two-layout selection should apply.
|
||
|
||
**Related work:**
|
||
- TRU-197 (`documentation/economic/invoice-template-audit.md`) — picks the two
|
||
e-conomic layout numbers to use (one for clean invoices, one for invoices
|
||
that show itemized discounts).
|
||
- TRU-193 (`documentation/economic/export-field-audit.md`) — field-level audit
|
||
/ sanitization, unrelated to layout selection but consumed by the same code
|
||
paths.
|
||
- PR #391 — `economic_export_sanitizer`, the sanitizer that all draft-line
|
||
paths now run their text through.
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
A draft invoice in this codebase is built in two phases:
|
||
|
||
1. **Create the draft envelope** — `POST /invoices/drafts` with a payload
|
||
that contains `customer`, `paymentTerms`, `layout.layoutNumber`,
|
||
`recipient`, `currency`, `date`, etc. This is the only place where
|
||
`layout.layoutNumber` is set on the draft.
|
||
2. **Add lines to the draft** — `POST /invoices/drafts/{id}/lines` with an
|
||
array of product / text / discount lines. Lines are added either one
|
||
order at a time (single-order draft flow) or in accumulated batches
|
||
(collected-invoice flow). The layout is **already fixed** at this point
|
||
and is not re-sent.
|
||
|
||
There are therefore only **two** code paths in the entire backend that
|
||
create the draft envelope and could pick a layout. Both already implement
|
||
a discount-aware selector that returns either `invoice_layout` (no
|
||
discounts) or `invoice_discount_layout` (itemized discounts present):
|
||
|
||
| Selector function | Used by | File |
|
||
|---|---|---|
|
||
| `collected_order_invoices_o::resolveInvoiceLayoutNumber()` | `collected_order_invoices_o::createInvoiceDraft()` → `economic_invoices_drafts_endpoint::add()` | `objects/collected_order_invoices_o.php:673` |
|
||
| `economic_invoice_draft_mo::resolveLayoutNumber()` | `economic_invoice_draft_mo::createInvoiceDraftExample()` | `modules/economic/invoices/draft/economic_invoice_draft_mo.php:115` |
|
||
|
||
The two selectors are independent implementations of the same idea. They
|
||
both:
|
||
|
||
1. Inspect the lines that will be sent (or the orders that will be added
|
||
to the draft).
|
||
2. If any line / order has a non-zero `discountPercentage` (or, in the
|
||
collected-invoice path, any "billable discount" per
|
||
`economic_invoice_draft::orderItemHasBillableDiscount()`), return
|
||
`invoice_discount_layout`.
|
||
3. Otherwise return `invoice_layout`.
|
||
4. Throw a `RuntimeException` / `Exception` if the discount layout is
|
||
required but `invoiceDiscountLayoutNumber` is unconfigured (≤ 0).
|
||
|
||
The two config variables are defined in:
|
||
|
||
- `services/nginx/app/modules/economic/config/economic_invoice_layout_c.php`
|
||
— `invoiceLayoutNumber`, `int`, **required** (default `1`).
|
||
- `services/nginx/app/modules/economic/config/economic_invoice_discount_layout_c.php`
|
||
— `invoiceDiscountLayoutNumber`, `int`, **optional** (default `null`).
|
||
- Both are wired into `classes\economic::$config` via
|
||
`services/nginx/app/modules/economic/economic_c.php` lines 25–48.
|
||
|
||
> **Net result of the audit:** the two-layout selection is already
|
||
> implemented in both places where a draft envelope is created. There is
|
||
> **no** code path that creates a draft without going through one of these
|
||
> two selectors. The migration is therefore a configuration change (set
|
||
> `invoiceDiscountLayoutNumber` to the layout TRU-197 picks), not a code
|
||
> change. See §5 *Migration plan* for the small set of files that still
|
||
> touch the layout topic and may need follow-up.
|
||
|
||
---
|
||
|
||
## 1. Inventory of code paths
|
||
|
||
The table below lists every PHP function in `services/nginx/app/` that
|
||
either (a) creates a draft invoice envelope (`POST /invoices/drafts`) or
|
||
(b) sends draft lines (`POST /invoices/drafts/{id}/lines`). Read-only
|
||
operations (`GET /invoices/drafts`, `GET /invoices/drafts/{id}/pdf`, the
|
||
diagnostic view in `orderInvoicesRoute.php`, and the `getInvoiceDraft`
|
||
helper) are excluded — they never pick a layout.
|
||
|
||
| # | File:line | Function | What it does | Picks layout? | Layout used | Discount-aware? | Recommendation |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 1 | `modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php:107` | `economic_invoices_drafts_endpoint::add()` | Low-level `POST /invoices/drafts` envelope builder; accepts an optional `$layout_number` arg. | **Yes (caller-driven).** Sets `layout.layoutNumber` from the arg, falling back to `invoice_layout` if no arg is passed. | `invoice_layout` (default) or whatever the caller passes. | **No** — does not inspect lines. | Keep as-is. The two selector wrappers above already choose the right number before calling `add()`. |
|
||
| 2 | `modules/economic/invoices/draft/economicInvoicesDrafts.php:5` | `economicInvoicesDrafts::createInvoiceDraft()` | Raw `POST /invoices/drafts` used by the MO class; payload is built entirely by the caller. | **No (caller-driven).** The `data` array the caller passes must already contain `layout.layoutNumber`. | Whatever the caller put in `data['layout']['layoutNumber']`. | No. | Keep as-is. Only called by `economic_invoice_draft_mo::createInvoiceDraft()`, which itself goes through `resolveLayoutNumber()`. |
|
||
| 3 | `modules/economic/invoices/draft/economic_invoice_draft_mo.php:45` | `economic_invoice_draft_mo::createInvoiceDraftExample()` | The single-order draft envelope builder. Builds the full payload including `lines` and `layout.layoutNumber`, then calls `createInvoiceDraft()`. | **Yes — discount-aware.** Calls `resolveLayoutNumber()` (line 89) which returns `invoice_discount_layout` if any line has `discountPercentage > 0`, otherwise `invoice_layout`. | `invoice_layout` (no discount) or `invoice_discount_layout` (with discount). | **Yes** via `hasDiscountedItemizedLines()` (line 130). | **Already correct.** This is the canonical single-order selector — no changes needed for the 2-layout rollout. |
|
||
| 4 | `modules/economic/invoices/draft/economic_invoice_draft_mo.php:115` | `economic_invoice_draft_mo::resolveLayoutNumber()` (private) | The selector for path #3. | Yes. | `invoice_layout` or `invoice_discount_layout`. | Yes. | Keep as-is. |
|
||
| 5 | `modules/economic/invoices/draft/economic_invoice_draft_mo.php:130` | `economic_invoice_draft_mo::hasDiscountedItemizedLines()` (private) | Line scan: any line with `product` set and `discountPercentage > 0`. | n/a (read-only) | n/a | Yes. | Keep as-is. |
|
||
| 6 | `modules/economic/invoices/draft/economic_invoice_draft_mo.php:151` | `economic_invoice_draft_mo::createInvoiceDraft()` | Thin wrapper around `economicInvoicesDrafts::createInvoiceDraft()`. | No (caller-driven). | Whatever the caller put in `$data`. | No. | Keep as-is. |
|
||
| 7 | `modules/economic/invoices/draft/economic_invoice_draft_mo.php:209` | `economic_invoice_draft_mo::addLinesToInvoiceDraft()` | `POST /invoices/drafts/{id}/lines` — adds already-buffered `$this->lines` to an existing draft. | **No** — the draft's layout is already set when it was created. | Whatever the draft was created with. | n/a. | No change. Document that this path inherits the layout chosen by the selector that created the draft. |
|
||
| 8 | `modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php:114` | `economic_invoices_draft_endpoint::add_lines()` | Raw `POST /invoices/drafts/{id}/lines` with caller-supplied `$draft_lines`. | No. | n/a. | n/a. | No change. |
|
||
| 9 | `modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php:75` | `economic_invoices_draft_endpoint::add_orders()` | Iterates over `orders_o[]` and adds them to an existing draft via `economic_invoice_draft` (helper). Batched. | No. | n/a. | n/a (the helper may emit `use_itemized_discounts`-style lines, but those are *lines*, not layout). | No change. |
|
||
| 10 | `modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php:144` | `economic_invoices_draft_endpoint::add_environmental_and_oil_fees()` | Adds env/oil fee product lines to an existing draft. | No. | n/a. | n/a. | No change. |
|
||
| 11 | `modules/economic/helpers/economic_invoice_draft.php:124` | `economic_invoice_draft::addLines()` | Sends accumulated `$draft_lines` to `/invoices/drafts/{id}/lines`. Optionally runs preflight validation. | No. | n/a. | n/a. | No change. |
|
||
| 12 | `modules/economic/helpers/economic_invoice_draft.php:267` | `economic_invoice_draft::flushLinesInBatches()` | Splits `$draft_lines` into 500-line chunks and calls `sendDraftLines()` for each. | No. | n/a. | n/a. | No change. |
|
||
| 13 | `classes/economic_transfer_executor.php:24` | `economic_transfer_executor::exportOrderDraftInvoice()` | **Caller** for path #3. Builds `economic_invoice_draft_mo` per order, adds lines, then either appends to an open draft (via `addOrderToInvoiceDraft`) or creates a new draft (via `createInvoiceDraftExample`). | Inherits path #3's selector. | `invoice_layout` or `invoice_discount_layout`. | Yes (via path #3). | No change. |
|
||
| 14 | `classes/economic_transfer_executor.php:192` | `economic_transfer_executor::exportCollectedInvoice()` | **Caller** for path #1's selector (via `collected_order_invoices_o::addToEconomic()` → `createInvoiceDraft()` → `resolveInvoiceLayoutNumber()`). | Inherits path #1's selector. | `invoice_layout` or `invoice_discount_layout`. | Yes (via path #15). | No change. |
|
||
| 15 | `classes/economic_transfer_executor.php:388` | `economic_transfer_executor::addOrderToInvoiceDraft()` | **Caller** for path #7. Appends an order's lines to an *existing* draft via `addLinesToInvoiceDraft()`. | No — draft already has a layout. | n/a. | n/a. | No change. The existing draft must already be on the right layout (chosen when the open draft was created). |
|
||
| 16 | `objects/collected_order_invoices_o.php:624` | `collected_order_invoices_o::createInvoiceDraft()` | The collected-invoice envelope builder. Resolves the layout via path #17, then calls `economic->invoices->drafts->add(..., $layout_number)`. | **Yes — discount-aware.** | `invoice_layout` or `invoice_discount_layout`. | Yes (via path #18). | **Already correct.** Canonical collected-invoice selector. |
|
||
| 17 | `objects/collected_order_invoices_o.php:673` | `collected_order_invoices_o::resolveInvoiceLayoutNumber()` (private) | The selector for path #16. | Yes. | `invoice_layout` or `invoice_discount_layout`. | Yes (via path #18). | Keep as-is. |
|
||
| 18 | `objects/collected_order_invoices_o.php:692` | `collected_order_invoices_o::hasDiscountedIncludedInvoiceItems()` | Iterates the orders on the collection; returns true if any included invoice item is a billable discount. | n/a (read-only) | n/a | Yes. | Keep as-is. |
|
||
| 19 | `objects/collected_order_invoices_o.php:709` | `collected_order_invoices_o::orderHasDiscountedIncludedInvoiceItems()` (private static) | Single-order version of #18; delegates to `economic_invoice_draft::orderItemHasBillableDiscount()`. | n/a (read-only) | n/a | Yes. | Keep as-is. |
|
||
| 20 | `objects/collected_order_invoices_o.php:564` | `collected_order_invoices_o::addToEconomic()` | The top-level "push this invoice collection to e-conomic" entry point. Calls path #16 then path #21. | Inherits path #16. | `invoice_layout` or `invoice_discount_layout`. | Yes. | No change. |
|
||
| 21 | `objects/collected_order_invoices_o.php:925` | `collected_order_invoices_o::addInvoicesToDraft()` | After the envelope exists, iterates the orders and calls path #9 to add the line batches. | No — line-add path. | n/a. | n/a. | No change. |
|
||
| 22 | `routes/economicInvoiceRoute.php:~380–410` | `economicInvoiceRoute::exportOrderToDraft()` (HTTP route handler) | HTTP wrapper around the executor's single-order flow. Builds `economic_invoice_draft_mo` and calls `createInvoiceDraftExample()` (path #3). | Inherits path #3. | `invoice_layout` or `invoice_discount_layout`. | Yes. | No change. |
|
||
|
||
**Read-only paths (excluded from the migration list):**
|
||
|
||
- `modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php:21` — `get(int $invoice_id)`
|
||
- `modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php:39` — `get_from_external_id(string $external_id)`
|
||
- `modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php:35` — `get(array $filters, array $pagination)`
|
||
- `modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php:60` — `get_all()`
|
||
- `modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php:73` — `get_invoice_lines(array $invoice_ids, array $filters)`
|
||
- `modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php:201` — `exists(int $draft_invoice_number)`
|
||
- `modules/economic/invoices/draft/economic_invoice_draft_mo.php:200` — `getInvoiceDraft(int $int)`
|
||
- `modules/economic/invoices/draft/economic_invoice_draft_mo.php:170` — `getInvoicePdf(int $param)`
|
||
- `modules/economic/invoices/draft/economic_invoice_draft_mo.php:160` — `deleteInvoiceDraft(int $value)`
|
||
- `modules/economic/invoices/draft/economic_invoice_draft_mo.php:166` — `publishInvoiceDraft(int $invoiceDraftId)` — **important**: this is the *book* step (`POST /invoices/booked` with `{draftInvoice:{draftInvoiceNumber:N}}`). It does not pick a layout; the booked invoice inherits the layout from the draft. Keep as-is.
|
||
- `routes/orderInvoicesRoute.php:2178` — diagnostic fetch (`$economic->invoices->draft->get(...)`)
|
||
- `modules/economic/helpers/economic_tasks.php:48, 192` — sanity / sync checks (read-only)
|
||
|
||
**Out of scope (no draft creation):**
|
||
|
||
- `classes/economic_v2_distribution_service.php` — distribution *reporting*
|
||
(read-only aggregations over booked invoices). Never creates a draft.
|
||
- `modules/economic/helpers/economic_invoice_booked.php` — the booked-invoice
|
||
data class. No HTTP calls.
|
||
|
||
---
|
||
|
||
## 2. Current state
|
||
|
||
- **Both** envelope creators (path #3 / `createInvoiceDraftExample` and path
|
||
#16 / `createInvoiceDraft`) already have a working discount-aware selector
|
||
that returns one of two layout numbers from the config store.
|
||
- The selectors read from the same two config variables
|
||
(`invoiceLayoutNumber` and `invoiceDiscountLayoutNumber`) which are
|
||
already wired into `economic::$config` and surfaced in the
|
||
`EconomicConfigEntry` OpenAPI schema.
|
||
- The `invoiceDiscountLayoutNumber` config var is currently **optional**
|
||
(see `economic_invoice_discount_layout_c.php` — `setupConfigVariable(...,
|
||
true, ...)` with `required = true` in the call signature but the
|
||
constructor's third arg `false` means a null value is allowed; the
|
||
selectors throw if it is required and ≤ 0).
|
||
- The selectors are independent code paths. They each inspect lines
|
||
slightly differently:
|
||
- The MO selector (`hasDiscountedItemizedLines`) checks
|
||
`discountPercentage > 0` per line.
|
||
- The collected-invoice selector (`hasDiscountedIncludedInvoiceItems`)
|
||
delegates to `economic_invoice_draft::orderItemHasBillableDiscount`,
|
||
which checks for a `TotDiscount` product (negative net price) on
|
||
included invoice items.
|
||
- Both reach the same boolean result: *does this draft need the discount
|
||
layout?* — so the layout chosen by either selector is consistent.
|
||
|
||
---
|
||
|
||
## 3. Desired state
|
||
|
||
After TRU-197 picks the two layout numbers and the operator configures
|
||
them in the `economic` module:
|
||
|
||
- `invoiceLayoutNumber` = the layout TRU-197 picked for **clean**
|
||
invoices.
|
||
- `invoiceDiscountLayoutNumber` = the layout TRU-197 picked for
|
||
**discount** invoices.
|
||
|
||
Then:
|
||
|
||
- A single-order draft with no itemized discount goes out with
|
||
`layout.layoutNumber = invoiceLayoutNumber` (path #3 / selector #4).
|
||
- A single-order draft with an itemized discount goes out with
|
||
`layout.layoutNumber = invoiceDiscountLayoutNumber` (path #3 / selector
|
||
#4).
|
||
- A collected-invoice draft with no billable discount goes out with
|
||
`invoiceLayoutNumber` (path #16 / selector #17).
|
||
- A collected-invoice draft with a billable discount goes out with
|
||
`invoiceDiscountLayoutNumber` (path #16 / selector #17).
|
||
|
||
No code changes are required to achieve this — only the two config
|
||
variables need to be set in the `economic` module (and validated by
|
||
the superuser status probe at `superuser_system_status_service.php:866`).
|
||
|
||
---
|
||
|
||
## 4. Migration plan
|
||
|
||
Because the selectors already exist, the migration is a **configuration
|
||
rollout** plus a small handful of defensive tasks. Files to touch:
|
||
|
||
### 4.1 Required for rollout
|
||
|
||
- **`services/nginx/app/modules/economic/config/economic_invoice_layout_c.php`**
|
||
— confirm `invoiceLayoutNumber` is configured to TRU-197's "clean" layout.
|
||
- **`services/nginx/app/modules/economic/config/economic_invoice_discount_layout_c.php`**
|
||
— set `invoiceDiscountLayoutNumber` to TRU-197's "discount" layout. (The
|
||
constructor signature already allows this to be a non-required variable,
|
||
but the selectors will throw a `RuntimeException` / `Exception` if the
|
||
discount layout is required and the value is 0 or null — so the rollout
|
||
must include setting this var in every environment.)
|
||
|
||
### 4.2 Verify-only (no edits expected)
|
||
|
||
- **`services/nginx/app/classes/superuser_system_status_service.php:866`**
|
||
— already lists `invoiceLayoutNumber` and `invoiceDiscountLayoutNumber`
|
||
as required keys for the `economic` module probe. Confirm the probe
|
||
treats `invoiceDiscountLayoutNumber` as required and surfaces a clear
|
||
error when missing (it currently appears in the `required` array, which
|
||
is the correct behavior).
|
||
- **`services/nginx/app/openapi.yaml:18644`** — `EconomicConfigEntry.variable`
|
||
enum already includes `invoiceDiscountLayoutNumber`. No change.
|
||
- **`services/nginx/app/tests/Unit/SystemStatus/SuperuserSystemStatusServiceTest.php:893–894`**
|
||
— test fixtures already cover both layout config vars. Confirm values
|
||
match TRU-197's picks.
|
||
|
||
### 4.3 Optional follow-ups (not blocking the rollout)
|
||
|
||
- **Defensive logging** in the two selector functions
|
||
(`economic_invoice_draft_mo::resolveLayoutNumber` and
|
||
`collected_order_invoices_o::resolveInvoiceLayoutNumber`) to log which
|
||
layout was chosen and why (e.g.
|
||
`[TRU-198] draft {id} uses discount layout (3 discounted lines)`).
|
||
This is useful for post-rollout verification in the e-conomic UI.
|
||
- **A single, shared selector helper** that both paths use, to avoid
|
||
drift between the two private selectors. Recommended location:
|
||
`services/nginx/app/modules/economic/helpers/economic_invoice_draft.php`
|
||
or a new
|
||
`services/nginx/app/modules/economic/helpers/economic_invoice_layout_resolver.php`.
|
||
Out of scope for the configuration rollout; consider for a follow-up
|
||
refactor.
|
||
- **E2E / integration test** that:
|
||
1. Creates a single-order draft with at least one discounted line and
|
||
asserts the resulting draft's `layout.layoutNumber` equals
|
||
`invoiceDiscountLayoutNumber`.
|
||
2. Creates a single-order draft with no discounted lines and asserts
|
||
`invoiceLayoutNumber`.
|
||
3. Creates a collected-invoice draft with at least one
|
||
`TotDiscount` line and asserts `invoiceDiscountLayoutNumber`.
|
||
4. Creates a collected-invoice draft with no `TotDiscount` lines and
|
||
asserts `invoiceLayoutNumber`.
|
||
See `tests/Unit/Invoicing/EconomicDraftCustomerOpenApiSpecTest.php` and
|
||
`EconomicLegacyDraftPayloadWiringTest.php` for the existing patterns.
|
||
|
||
### 4.4 Files that explicitly need NO changes
|
||
|
||
- `services/nginx/app/classes/economic_v2_distribution_service.php` —
|
||
distribution reporting, not a draft creator.
|
||
- `services/nginx/app/modules/economic/helpers/economic_invoice_booked.php`
|
||
— booked-invoice data class.
|
||
- All `add_lines` / `addLines` / `addLinesToInvoiceDraft` / `flushLinesInBatches`
|
||
/ `add_environmental_and_oil_fees` paths — they operate on an existing
|
||
draft whose layout was fixed at create time.
|
||
|
||
---
|
||
|
||
## 5. Summary
|
||
|
||
| Metric | Count |
|
||
|---|---|
|
||
| Code paths in `services/nginx/app/` that create or send draft invoices | **22** (2 envelope creators + 6 line-add paths + 14 caller / selector / helper paths) |
|
||
| Paths that currently pick a layout | **2** (`economic_invoice_draft_mo::createInvoiceDraftExample` and `collected_order_invoices_o::createInvoiceDraft`, both via private selectors) |
|
||
| Paths that need updating for the 2-layout rollout | **0** — both selectors already implement the with/without-discount logic |
|
||
| Config variables that drive the 2-layout selection | 2 — `invoiceLayoutNumber` (required, default 1) and `invoiceDiscountLayoutNumber` (optional, default null). Already wired into `economic::$config` and the OpenAPI schema. |
|
||
| Files that need editing for the rollout | 2 — `economic_invoice_layout_c.php` and `economic_invoice_discount_layout_c.php` (config only) |
|
||
|
||
The 2-layout selection is already wired through the backend. The TRU-198
|
||
investigation confirms that the rollout reduces to setting the two
|
||
`invoice*LayoutNumber` config variables to the layout numbers TRU-197
|
||
picks, plus optional defensive logging and an E2E test for verification.
|