Skip to content

feat(templates): add the ConsultingInvoice preset and the structured invoice model - #611

Merged
DemchaAV merged 3 commits into
feature/template-promotionfrom
feat/consulting-invoice-preset
Aug 30, 2026
Merged

feat(templates): add the ConsultingInvoice preset and the structured invoice model#611
DemchaAV merged 3 commits into
feature/template-promotionfrom
feat/consulting-invoice-preset

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

The invoice family renders one look, and its data layer knows one shape: InvoiceData carries an invoice as pre-formatted display strings — one address block per party, line items whose quantity and money are already rendered, a flat list of summary rows. The professional-services invoice cannot be said in it: a brand lockup with the sender's own logo, labelled masthead metadata, priced lines with a service period and a unit each, a tax breakdown over numeric figures, bank payment fields. That look exists today only as a published standalone template pinned to graph-compose:2.2.0 with its assets on the filesystem.

Commits, in review order

1. 78d0f9b0 — the structured invoice document model. templates.data.invoice gains a second model beside the display one: StructuredInvoiceData (fluent builder) wrapped by StructuredInvoiceDocumentSpec, with InvoiceBrand, InvoiceContactBlock, InvoiceMasthead (+Entry, one flag for the emphasized due date), InvoiceRecipient, InvoiceSummaryBlock, InvoiceServiceLines (+Columns, +Line with BigDecimal quantity/price/amount and the unit), InvoiceTotalsBlock (+Row, and the total as its own labelled band rather than the last row by convention), InvoicePaymentBlock (+Field) and InvoiceNotesBlock. Labels and headings are content, so one composition prints another business's wording without a fork. The brand logo arrives as DocumentImageData — it is caller-supplied content, not template chrome — and stays optional. Normalization matches the family's existing records: null → empty form, money and quantities → zero, collections frozen. The display model is untouched; each near-duplication with it is justified in Javadoc.

2. 92903f74 — the ConsultingInvoice preset. A DocumentTemplate<StructuredInvoiceDocumentSpec> via create(), split for the 500-LOC rule into package-private ConsultingStyles (measured geometry and the Poppins scale), ConsultingText, ConsultingIcons, ConsultingMasthead, ConsultingBody and ConsultingClosing. It owns its session geometry — A4, the margin published as RECOMMENDED_MARGIN, the page fills — and draws the legal line and page numbers as FOOTER-zone chrome, so they repeat when the table paginates. The legal line is composed from the supplier's registered name and registration rather than restated in a second record. The six contact/bank/calendar marks ship inside the artifact under templates/invoice/consulting/icons/.

Named for the look, not the sample company: the family already reads ModernInvoice / ClassicInvoice, and the ported bundle's brand is fixture data.

Verification

Full reactor gate (the eight CI modules) → BUILD SUCCESS; consulting qa gates 12/12; examples suite 75/75 including CommittedAssetDriftTest with the new preview; javadoc:javadoc → 0 warnings.

Port parity, verified on one machine against the published template itself: the preset fed the template's own fixture is pixel-identical to the standalone render — 0 / 2,173,720 differing pixels — with node-identical geometry (95/95). The bundle also renders identically on its pinned 2.2.0 and on develop, so there is no engine drift to account for: the approved preview is the target.

Notes

  • The line-item description column is measured with AWT. A rich run cannot carry a line break, so the description is pushed onto its own line by a zero-height rectangle sized to the remaining width, which needs the rendered title width. The face it measures in ships in graph-compose-fonts, which the templates module does not depend on, so a caller that adds only the templates artifact measures in the platform's SansSerif — documented on the field, including that such a caller already renders the whole document in a substituted face. The engine does implement a hard break for inline runs (ParagraphWrapping.tokenizeInlineRuns) but sanitizes before splitting, so the newline is stripped first; swapping those two steps would retire this mechanism, and that is a shared-engine change for its own PR.
  • Guards added where the model documents a value as optional but the ported code printed it regardless: an omitted currency prints no (), an unregistered supplier no dangling separator, a recipient without an attention line no blank line, a brand without a qualifier no bare rules, and the bank band is sized from its field count rather than fixed at five fields. Each is asserted on the text layer.
  • The overflow snapshot freezes page count and block geometry; the table's own rows are a leaf node there, so the column ratios, zebra parity and cell anchors are guarded by the pixel gate and the text assertions instead.

Lane: canonical (templates.data.invoice + templates.invoice.presets + qa + examples) — no engine changes.

The invoice family's data layer knew one shape - an invoice as
pre-formatted display strings, with one address block per party, line
items whose quantity and money are already rendered, and a flat list of
summary rows. That cannot carry the structured business invoice: a
brand lockup with the sender's own logo, labelled masthead metadata,
a contact block with a business registration, priced service lines
carrying BigDecimal figures and the unit they are counted in, a totals
stack with its own total band, bank payment fields, and a footer line.

templates.data.invoice now carries that second model -
StructuredInvoiceData (+ its section records) wrapped by
StructuredInvoiceDocumentSpec - alongside the display one; a preset
consumes the model whose shape it renders. The brand logo arrives as
DocumentImageData: the logo is caller-supplied content, not template
chrome, and it stays optional so a wordmark-only lockup composes.
Every component normalizes null to its empty form, money and
quantities default to zero, and collections are frozen, matching the
family's existing records.
The invoice family rendered one look on a display model that carries an
invoice as pre-formatted strings. This adds both halves of the
structured business invoice: a second data model and the first preset
that renders it.

The model - StructuredInvoiceData and its section records, wrapped by
StructuredInvoiceDocumentSpec - carries a brand lockup with the
sender's own logo, labelled masthead metadata, a contact block with a
business registration, priced service lines with BigDecimal figures and
the unit they are counted in, a totals stack with its own total band,
and bank payment fields. The logo arrives as DocumentImageData because
it is caller-supplied content rather than template chrome, and it stays
optional. Every component normalizes null to its empty form, money and
quantities default to zero, and collections are frozen.

The preset - invoice/presets/ConsultingInvoice - composes the corporate
masthead over the priced lines, with the bank details beside the notes
and the due-by chip, and owns its session geometry: A4, the published
margin, the page fills, and the legal line and page numbers as footer
chrome. That line is composed from the supplier's registered name and
registration, so a document states them once. The contact marks, bank
badge and calendar ship inside the artifact; a document without a logo
falls back to the wordmark lockup, and a channel, currency,
registration or attention line the sender omits is left out rather than
printed empty.

qa: one fixtures class feeds the smoke, the exact layout snapshots
(single page and a 26-line overflow) and the pixel gate; the smoke
drives full layout and render and asserts the priced figures, the
repeated table header and the omission guards on the text layer, where
the snapshot cannot see inside the table. Examples gain
ConsultingInvoiceV2Example (invoice-consulting-v2) with its preview.
/**
* Normalizes optional fields and freezes the address lines.
*/
public InvoiceContactBlock {
Comment on lines +116 to +117
.addRow("Masthead", row -> row
.gap(HEADER_GAP)
Comment on lines +125 to +126
.addRow("PartiesAndSummary", row -> row
.gap(35)
Comment on lines +141 to +142
.addRow("PaymentAndNotes", row -> row
.gap(LOWER_GAP)
@DemchaAV
DemchaAV changed the base branch from develop to feature/template-promotion August 30, 2026 17:57
… feat/consulting-invoice-preset

# Conflicts:
#	CHANGELOG.md
#	examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
@DemchaAV
DemchaAV merged commit 8dd8934 into feature/template-promotion Aug 30, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the feat/consulting-invoice-preset branch August 30, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants