From 42cfe0c2b9aedcb1fbee3c3de64b29f83f6dd248 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Tue, 1 Sep 2026 08:21:15 +0100 Subject: [PATCH] feat(templates): let a billed party print its own registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A supplier could already state a tax registration through InvoiceContactBlock.taxRegistrationLabel / taxRegistrationNumber, and the party being billed could not — but most B2B invoices print the customer's VAT or tax number too, under its address. subline was the only spare field and it is contractually the attention line that sits ABOVE the address, so a registration put there renders in the wrong place. InvoiceRecipient now carries registrationLabel and registrationNumber, mirroring the pair the supplier block already has, with hasRegistration() for the presets that draw the row only when there is a number to draw. Both are plain strings, blank when absent, and the six-argument constructor is kept explicitly, so existing calls compile and link unchanged. --- CHANGELOG.md | 12 +++++++ .../StructuredInvoiceCompatibilityTest.java | 27 ++++++++++++++ .../data/invoice/InvoiceRecipient.java | 36 ++++++++++++++++++- 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 984bb6b37..67ab2e97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A billed party carries a printed registration.** A supplier could already state a + tax registration through `InvoiceContactBlock.taxRegistrationLabel` / + `taxRegistrationNumber`, and the party being billed could not — but most B2B invoices + print the customer's VAT or tax number too, under its address. `subline` was the only + spare field and it is contractually the attention line that sits *above* the address, + so a registration put there renders in the wrong place. `InvoiceRecipient` now carries + `registrationLabel` and `registrationNumber`, mirroring the pair the supplier block + already has, with `hasRegistration()` for the presets that draw the row only when + there is a number to draw. Both are plain strings, blank when absent, and the + six-argument constructor is kept explicitly, so existing calls compile and link + unchanged and every recipient built through them still prints no registration. + - **An invoice line carries a mark.** A design that opens each service line with a glyph — a card for a billing line, a shield for fraud screening, a globe for a hosted service — had nowhere to say which one, and deriving it from the description would diff --git a/qa/src/test/java/com/demcha/compose/document/templates/data/invoice/StructuredInvoiceCompatibilityTest.java b/qa/src/test/java/com/demcha/compose/document/templates/data/invoice/StructuredInvoiceCompatibilityTest.java index 5283b491f..76a7c3096 100644 --- a/qa/src/test/java/com/demcha/compose/document/templates/data/invoice/StructuredInvoiceCompatibilityTest.java +++ b/qa/src/test/java/com/demcha/compose/document/templates/data/invoice/StructuredInvoiceCompatibilityTest.java @@ -95,6 +95,33 @@ void aNullMarkNormalizesToBlankLikeTheFieldsBesideIt() { assertThat(line.icon()).isEmpty(); } + @Test + void theRecipientConstructorThatPredatesTheRegistrationLeavesItBlank() { + InvoiceRecipient recipient = new InvoiceRecipient("BILL TO", "Northwind Ltd.", "", + List.of("42 Bridgewater Street"), "Email", "ap@example.test"); + assertThat(recipient.registrationLabel()).isEmpty(); + assertThat(recipient.registrationNumber()).isEmpty(); + assertThat(recipient.hasRegistration()).isFalse(); + } + + @Test + void aRecipientCarriesTheRegistrationItIsGiven() { + InvoiceRecipient recipient = new InvoiceRecipient("BILL TO", "Northwind Ltd.", "", + List.of("42 Bridgewater Street"), "", "", "VAT Number", "GB 987 6543 21"); + assertThat(recipient.registrationLabel()).isEqualTo("VAT Number"); + assertThat(recipient.registrationNumber()).isEqualTo("GB 987 6543 21"); + assertThat(recipient.hasRegistration()).isTrue(); + } + + @Test + void aRecipientWithALabelAndNoNumberPrintsNothing() { + // The number is what makes the row worth drawing; a label on its own is + // a heading over an absence. + InvoiceRecipient recipient = new InvoiceRecipient("BILL TO", "Northwind Ltd.", "", + List.of(), "", "", "VAT Number", null); + assertThat(recipient.hasRegistration()).isFalse(); + } + @Test void theDataConstructorThatPredatesShipToLeavesItEmptyRatherThanNull() { // The record normalizes an absent block to its empty form, so preset diff --git a/templates/src/main/java/com/demcha/compose/document/templates/data/invoice/InvoiceRecipient.java b/templates/src/main/java/com/demcha/compose/document/templates/data/invoice/InvoiceRecipient.java index 777dddb9d..ae63c70a5 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/data/invoice/InvoiceRecipient.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/data/invoice/InvoiceRecipient.java @@ -19,6 +19,11 @@ * @param addressLines the address lines, in order * @param emailLabel the label printed before the email address * @param email the recipient's email address + * @param registrationLabel the label a printed registration is named by + * (e.g. {@code "VAT Number"}); blank when absent + * @param registrationNumber the registration itself, printed under the + * address rather than under the name; blank when + * absent */ public record InvoiceRecipient( String heading, @@ -26,7 +31,9 @@ public record InvoiceRecipient( String subline, List addressLines, String emailLabel, - String email) { + String email, + String registrationLabel, + String registrationNumber) { /** * Normalizes optional fields and freezes the address lines. @@ -38,5 +45,32 @@ public record InvoiceRecipient( addressLines = List.copyOf(Objects.requireNonNullElse(addressLines, List.of())); emailLabel = Objects.requireNonNullElse(emailLabel, ""); email = Objects.requireNonNullElse(email, ""); + registrationLabel = Objects.requireNonNullElse(registrationLabel, ""); + registrationNumber = Objects.requireNonNullElse(registrationNumber, ""); + } + + /** + * Backward-compatible constructor for callers that predate the printed + * registration. The recipient simply carries none. + * + * @param heading the block heading + * @param name the recipient's name + * @param subline the attention line under the name + * @param addressLines the address lines, in order + * @param emailLabel the label printed before the email address + * @param email the recipient's email address + */ + public InvoiceRecipient(String heading, String name, String subline, + List addressLines, String emailLabel, String email) { + this(heading, name, subline, addressLines, emailLabel, email, "", ""); + } + + /** + * Whether this recipient prints a registration under its address. + * + * @return {@code true} when the number is set + */ + public boolean hasRegistration() { + return !registrationNumber.isBlank(); } }