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(); } }