## 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>
336 lines
15 KiB
Markdown
336 lines
15 KiB
Markdown
# E-conomic Invoice Template Audit (TRU-197)
|
|
|
|
**Status:** Complete (no live call — credentials unavailable in this environment)
|
|
**Date:** 2026-08-17
|
|
**Scope:** Audit of the e-conomic invoice layouts available in the
|
|
`copenhagentruckwash/api` backend's e-conomic agreement, and the rationale for
|
|
the two-layout strategy (one for invoices **with** itemized discounts, one for
|
|
invoices **without**).
|
|
|
|
**Primary files audited:**
|
|
- `services/nginx/app/modules/economic/endpoints/economic_layouts_endpoint.php` (`GET /layouts`)
|
|
- `services/nginx/app/modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php` (draft invoice create — uses `layout.layoutNumber`)
|
|
- `services/nginx/app/modules/economic/invoices/draft/economic_invoice_draft_mo.php` (`resolveLayoutNumber()`)
|
|
- `services/nginx/app/objects/collected_order_invoices_o.php` (`resolveInvoiceLayoutNumber()`)
|
|
- `services/nginx/app/modules/economic/config/economic_invoice_layout_c.php` (`invoiceLayoutNumber` config var, default `1`)
|
|
- `services/nginx/app/modules/economic/config/economic_invoice_discount_layout_c.php` (`invoiceDiscountLayoutNumber` config var, default `1`)
|
|
- `services/nginx/app/routes/economicLayoutsRoute.php` (superuser `/economic/layouts` proxy)
|
|
|
|
---
|
|
|
|
## TL;DR — Recommendation
|
|
|
|
| Variant | Layout (configured) | Env-var name to set | Layout intent |
|
|
|---------|---------------------|---------------------|---------------|
|
|
| **With discounts** | `invoiceDiscountLayoutNumber` (currently `6` in `SuperuserSystemStatusServiceTest` fixtures; site default `1`) | `ECONOMIC_LAYOUT_WITH_DISCOUNTS` | Itemized lines with the `Rabat` line clearly visible (negative `unitNetPrice` for `TotDiscount` product) |
|
|
| **Without discounts** | `invoiceLayoutNumber` (currently `1` in tests and config default) | `ECONOMIC_LAYOUT_WITHOUT_DISCOUNTS` | Standard invoice, no discount clutter |
|
|
|
|
The two layout numbers above are **placeholders** to be confirmed by the
|
|
account admin in e-conomic. They are written into the runtime config variables
|
|
`invoiceLayoutNumber` and `invoiceDiscountLayoutNumber` (see env-var mapping
|
|
section below).
|
|
|
|
---
|
|
|
|
## 1. Why a 2-layout strategy is needed
|
|
|
|
The `copenhagentruckwash/api` backend already has plumbing for two invoice
|
|
layouts (see §4 below). The trigger to pick a layout is whether the invoice
|
|
**contains an itemized discount line** (a line with `product.productNumber =
|
|
"TotDiscount"` and a negative `unitNetPrice`, as produced by the
|
|
`Rabat` aggregator in `economic_invoice_draft`).
|
|
|
|
When such a line is present, the system routes the invoice through
|
|
`invoiceDiscountLayoutNumber`; otherwise it falls back to
|
|
`invoiceLayoutNumber`. The audit goal is to find the two layouts in e-conomic
|
|
that match these two intents (clean invoice vs. one that shows discounts
|
|
itemized).
|
|
|
|
---
|
|
|
|
## 2. Available e-conomic API for layouts
|
|
|
|
### 2.1 Endpoint
|
|
|
|
```
|
|
GET https://restapi.e-conomic.com/layouts
|
|
```
|
|
|
|
### 2.2 Auth headers (same as every other e-conomic call)
|
|
|
|
```
|
|
X-AppSecretToken: <ECONOMIC_API_APP_SECRET_TOKEN>
|
|
X-AgreementGrantToken: <ECONOMIC_API_APP_ACCESS_GRANT>
|
|
Content-Type: application/json
|
|
```
|
|
|
|
### 2.3 Response shape
|
|
|
|
The endpoint already exists in the codebase at
|
|
`services/nginx/app/modules/economic/endpoints/economic_layouts_endpoint.php`,
|
|
and is exposed to superusers via
|
|
`services/nginx/app/routes/economicLayoutsRoute.php` (`GET /economic/layouts`).
|
|
The PHP wrapper returns the raw JSON decoded into a stdClass:
|
|
|
|
```json
|
|
{
|
|
"collection": [
|
|
{
|
|
"layoutNumber": 1,
|
|
"name": "Standard",
|
|
"deleted": false,
|
|
"self": "https://restapi.e-conomic.com/layouts/1"
|
|
},
|
|
{
|
|
"layoutNumber": 12,
|
|
"name": "Rabat variant",
|
|
"deleted": false,
|
|
"self": "https://restapi.e-conomic.com/layouts/12"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
The minimal documented fields per layout are:
|
|
|
|
| Field | Type | Description |
|
|
|----------------|---------|-------------|
|
|
| `layoutNumber` | integer | Unique identifier of the layout. This is the value that goes in `layout.layoutNumber` on `/invoices/drafts`. |
|
|
| `name` | string | Display name configured in e-conomic (Settings → Design and Layouts). Up to ~100 chars. |
|
|
| `deleted` | boolean | `true` = layout is deleted and cannot be used. Filter these out. |
|
|
| `self` | string (uri) | Link reference to the layout resource (for `GET /layouts/:layoutNumber`). |
|
|
|
|
> Note: e-conomic layouts do **not** have an `isDefault` field. The "default"
|
|
> concept in e-conomic is per-customer-group, not global. To find the agreement
|
|
> default, query `/customers?filter=...` and look at the layout referenced on
|
|
> each customer group's default. For our purposes, the admin picks the two
|
|
> layout numbers we want to use, so no defaulting logic is required.
|
|
|
|
### 2.4 Example curl (run with real creds)
|
|
|
|
```bash
|
|
curl -sS -X GET "https://restapi.e-conomic.com/layouts" \
|
|
-H "X-AppSecretToken: $ECONOMIC_API_APP_SECRET_TOKEN" \
|
|
-H "X-AgreementGrantToken: $ECONOMIC_API_APP_ACCESS_GRANT" \
|
|
-H "Content-Type: application/json" \
|
|
| jq '.collection[] | {layoutNumber, name, deleted}'
|
|
```
|
|
|
|
### 2.5 Example Python (run with real creds)
|
|
|
|
```python
|
|
import os, requests
|
|
r = requests.get(
|
|
"https://restapi.e-conomic.com/layouts",
|
|
headers={
|
|
"X-AppSecretToken": os.environ["ECONOMIC_API_APP_SECRET_TOKEN"],
|
|
"X-AgreementGrantToken": os.environ["ECONOMIC_API_APP_ACCESS_GRANT"],
|
|
"Content-Type": "application/json",
|
|
},
|
|
timeout=15,
|
|
)
|
|
r.raise_for_status()
|
|
for layout in r.json()["collection"]:
|
|
print(layout["layoutNumber"], layout["name"], "deleted=" + str(layout["deleted"]))
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Live call — was it made?
|
|
|
|
**No.** This audit was run in a sandbox that does not have
|
|
`ECONOMIC_API_APP_SECRET_TOKEN` or `ECONOMIC_API_APP_ACCESS_GRANT` set (the
|
|
only available secrets are the GitHub PAT, Linear API key, and Slack tokens).
|
|
A live `GET /layouts` call would have returned `401 Unauthorized` at best, and
|
|
would have polluted the e-conomic log with a noisy failed request at worst.
|
|
The two layout numbers used by the test fixtures
|
|
(`SuperuserSystemStatusServiceTest`) — `1` and `6` — are taken as the
|
|
**configured** values that need to be **confirmed** by the e-conomic account
|
|
admin and, if changed, written into the e-conomic module config (see §4.3
|
|
env-var mapping).
|
|
|
|
To complete the live portion of the audit, run the curl above from a
|
|
machine that has the credentials (e.g. a developer laptop or a CI runner with
|
|
the secrets mounted). Paste the output into §6 of this doc and commit.
|
|
|
|
---
|
|
|
|
## 4. Current code state
|
|
|
|
### 4.1 Where layouts are read at runtime
|
|
|
|
* `services/nginx/app/modules/economic/invoices/draft/economic_invoice_draft_mo.php`
|
|
— `resolveLayoutNumber()` (line 115): returns either
|
|
`invoice_layout` or `invoice_discount_layout` depending on whether the draft
|
|
contains a `discountPercentage > 0` product line.
|
|
* `services/nginx/app/objects/collected_order_invoices_o.php`
|
|
— `resolveInvoiceLayoutNumber()` (line 673): same logic for collected
|
|
(batched) invoices. Trigger is `hasDiscountedIncludedInvoiceItems()`.
|
|
* `services/nginx/app/modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php`
|
|
— direct `/invoices/drafts` create with an explicit `layoutNumber` arg
|
|
(default = `invoice_layout`).
|
|
|
|
### 4.2 Where layouts are configured
|
|
|
|
* `services/nginx/app/modules/economic/config/economic_invoice_layout_c.php`
|
|
registers the `invoiceLayoutNumber` module config variable (default `1`,
|
|
required). This is the "no-discount" layout.
|
|
* `services/nginx/app/modules/economic/config/economic_invoice_discount_layout_c.php`
|
|
registers the `invoiceDiscountLayoutNumber` module config variable (default
|
|
`1`, optional, must be `> 0` to enable). This is the "with-discount" layout.
|
|
|
|
Both values are admin-editable at runtime via the standard module config
|
|
admin UI. The system status probe also lists them as required:
|
|
`services/nginx/app/classes/superuser_system_status_service.php` (line 866
|
|
key `invoiceDiscountLayoutNumber`; line 893-894 of the test fixture uses
|
|
`1` / `6`).
|
|
|
|
### 4.3 Env-var mapping
|
|
|
|
The module config values are stored in the `module_config` DB table, **not**
|
|
in environment variables. The contract is:
|
|
|
|
| Runtime value | Source | Where it's set |
|
|
|------------------------------------------------|-----------------------|---------------------------------------------------------------|
|
|
| `invoiceLayoutNumber` (without discounts) | Admin-set via UI | `services/nginx/app/modules/economic/config/economic_invoice_layout_c.php` |
|
|
| `invoiceDiscountLayoutNumber` (with discounts) | Admin-set via UI | `services/nginx/app/modules/economic/config/economic_invoice_discount_layout_c.php` |
|
|
|
|
The `ECONOMIC_API_APP_*` env vars are the **credentials** for talking to
|
|
e-conomic — they have no relationship to the layout-number config values.
|
|
|
|
That said, the task description asks for two env-var-style placeholders.
|
|
We will add the following **module-config aliases** (constants only, no
|
|
runtime logic yet) to `economic_layout_selector.php` (see §7) so that an
|
|
operator or a deployment automation can refer to them by name:
|
|
|
|
| Module-config constant | Friendly alias env-var-style name | Meaning |
|
|
|-----------------------------------|--------------------------------------|--------------------|
|
|
| `invoiceLayoutNumber` | `ECONOMIC_LAYOUT_WITHOUT_DISCOUNTS` | "Clean" layout, no discount clutter |
|
|
| `invoiceDiscountLayoutNumber` | `ECONOMIC_LAYOUT_WITH_DISCOUNTS` | Layout that itemizes the `Rabat` line clearly |
|
|
|
|
> If the deployment process is ever updated to read these from env vars
|
|
> instead of the module-config DB, the constant names in
|
|
> `economic_layout_selector.php` are the right place to wire that up.
|
|
|
|
### 4.4 Existing PRs and related work
|
|
|
|
* PR #391 — the original sanitization fix (TRU-188 family). Adds the
|
|
`economic_export_sanitizer` class and per-field sanitization on the
|
|
draft invoice lines, recipient block, and references.
|
|
* TRU-193 — the second audit, this time on extra fields and preflight
|
|
validation. See `documentation/economic/export-field-audit.md` for the
|
|
full sanitization audit.
|
|
* TRU-197 (this audit) — picks the two specific layout numbers to use,
|
|
one for with-discount and one for without-discount, and documents how
|
|
to find them in e-conomic.
|
|
|
|
---
|
|
|
|
## 5. Visual differences (to be verified)
|
|
|
|
Layouts in e-conomic are visually configured in the **Settings → Design and
|
|
Layouts** UI; the REST API only exposes their names and numbers, not their
|
|
visual representation. From the existing example invoice
|
|
(`services/nginx/app/routes/orderInvoicesRoute.php` line 199 sample payload),
|
|
a **booked** invoice with discounts has this structure:
|
|
|
|
```
|
|
lines: [
|
|
{ lineNumber: 1, sortKey: 1, description: "[ 01/12/2025 00:00 PLENO #38679 ]" },
|
|
{ lineNumber: 2, sortKey: 2, description: "Reference:" },
|
|
{ lineNumber: 3, sortKey: 3, description: "# Vaskeabonnementer" },
|
|
{ lineNumber: 4, sortKey: 4, description: "Trækker", quantity: 2, unitNetPrice: 579, vatRate: 25, totalNetAmount: 1158, product: {productNumber: 1} },
|
|
{ lineNumber: 5, sortKey: 5, description: "Reference:" },
|
|
{ lineNumber: 6, sortKey: 6, description: "# EH89254" },
|
|
{ lineNumber: 7, sortKey: 7, description: "Spot Free- Lastbil", quantity: 2, unitNetPrice: 39, vatRate: 25, totalNetAmount: 78, product: {productNumber: 33} },
|
|
{ lineNumber: 8, sortKey: 8, description: "Reference:" },
|
|
{ lineNumber: 9, sortKey: 9, description: "# EH89254" },
|
|
{ lineNumber: 10, sortKey: 10, description: "Rabat", quantity: 1, unitNetPrice: -542, vatRate: 25, totalNetAmount: -542, product: {productNumber: "TotDiscount"} },
|
|
{ lineNumber: 11, sortKey: 11 }
|
|
]
|
|
```
|
|
|
|
This invoice was **booked** with `layoutNumber = 12` (per the sample in
|
|
`orderInvoicesRoute.php`). Layout #12 is therefore a known historical choice;
|
|
it predates the audit and is not necessarily the final answer.
|
|
|
|
The visual difference between layouts 1 (default) and 12 (discount) is **to
|
|
be verified** by exporting a sample invoice in each layout. The relevant
|
|
template knobs in e-conomic are:
|
|
|
|
* Whether the discount column is rendered.
|
|
* Whether the `Rabat` line is broken out vs. folded into the per-product
|
|
`discountPercentage`.
|
|
* The number of text/separator lines (the two layouts may differ in how
|
|
much spacing they show between products).
|
|
|
|
These are UI choices in the e-conomic admin; the backend has no insight into
|
|
which lines the layout chooses to render.
|
|
|
|
---
|
|
|
|
## 6. Live-call results — TO BE FILLED IN
|
|
|
|
_Paste the output of the curl in §2.4 below, then commit._
|
|
|
|
```
|
|
# layoutNumber name deleted
|
|
# ------------ ---------------------------- -------
|
|
# 1 Standard false
|
|
# 12 Rabat variant false
|
|
# ...
|
|
```
|
|
|
|
Once filled in, mark the audit as **Verified — live call** and add a row
|
|
per layout to the table in §3.1 if the layout count is larger than
|
|
expected.
|
|
|
|
---
|
|
|
|
## 7. Files added in this PR
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `documentation/economic/invoice-template-audit.md` | This document. |
|
|
| `services/nginx/app/classes/economic_layout_selector.php` | Skeleton class exposing the two layout-number constants (`LAYOUT_WITHOUT_DISCOUNTS`, `LAYOUT_WITH_DISCOUNTS`) and a `name()` helper. **No runtime logic yet** — the two existing `resolveLayoutNumber()` / `resolveInvoiceLayoutNumber()` call sites continue to read the module-config values directly. The skeleton is in place so that a follow-up PR can switch those call sites to `EconomicLayoutSelector::LAYOUT_*` without renaming the constants. |
|
|
|
|
The `economic_layout_selector.php` skeleton is **intentionally empty of
|
|
logic** per the task description ("skeleton — just the constants, no logic
|
|
yet"). Wiring it up to replace the two existing call sites is tracked
|
|
separately and is out of scope for TRU-197.
|
|
|
|
---
|
|
|
|
## 8. What we recommend the e-conomic admin do
|
|
|
|
1. Open e-conomic → Settings → Design and Layouts.
|
|
2. **Duplicate** the current "standard" layout (the one currently set as
|
|
`invoiceLayoutNumber`). Call the duplicate "Rabat variant" or similar.
|
|
3. In the duplicate, **ensure the discount column is shown** (so the
|
|
negative `Rabat` line we push as `TotDiscount` renders cleanly).
|
|
4. Note the `layoutNumber` of:
|
|
* The original (clean) layout → set as `invoiceLayoutNumber` in
|
|
`services/nginx/app/modules/economic/config/economic_invoice_layout_c.php`
|
|
(admin override, or via the module config UI).
|
|
* The duplicate (with-discounts) layout → set as
|
|
`invoiceDiscountLayoutNumber` in
|
|
`services/nginx/app/modules/economic/config/economic_invoice_discount_layout_c.php`.
|
|
5. Book a test invoice with a discount and a test invoice without, and
|
|
confirm the PDF looks right in each case.
|
|
|
|
---
|
|
|
|
## 9. Refs
|
|
|
|
* TRU-188 — original 400 on `/` in order reference (PR #391)
|
|
* TRU-193 — second-wave audit on extra fields, preflight validation
|
|
(`documentation/economic/export-field-audit.md`)
|
|
* PR #391 — initial sanitization fix
|
|
* `services/nginx/app/modules/economic/endpoints/economic_layouts_endpoint.php`
|
|
— `GET /layouts` wrapper
|
|
* `services/nginx/app/modules/economic/invoices/draft/economic_invoice_draft_mo.php`
|
|
— `resolveLayoutNumber()` for single draft invoices
|
|
* `services/nginx/app/objects/collected_order_invoices_o.php`
|
|
— `resolveInvoiceLayoutNumber()` for collected (batched) invoices
|
|
* E-conomic REST API docs: https://restdocs.e-conomic.com/ (search "Layouts")
|