Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,35 @@ follow semantic versioning; release dates are ISO 8601.

### Templates

- **A paginating invoice preset: `PaymentsInvoice`.** A lavender-and-navy sheet with a
diagonal band crossing the masthead, a half-split issuer and metadata header, two
addressed parties on discs, a marked service-line table, a settlement row pairing bank
details against the totals, a note block and a two-cell document footer. Ships as
`invoice.presets.PaymentsInvoice` on the existing `StructuredInvoiceData` model,
porting the rendered layout of a published standalone template. It is the first preset
in the family built to **flow**: the design shows six service lines and a real billing
month brings dozens, so the table's header repeats on every page it reaches, a
continuation page reserves a deeper bottom margin than page one, and every page
carries its number — a financial record that runs over has to make a missing page
detectable. The table is one column, not five: the design shows an outer box and a
rule between rows with no interior verticals, which is exactly what a single-column
table draws, and the five columns are a row inside each cell. Building it the other
way round would leave every continuation page ending in an empty bordered strip,
because a section's box fills its page fragment rather than hugging its rows. The
lockup beside the title is the caller's: the design's mark occupies a measured
136 × 55.3 px box, a document that brings a logo has it drawn to that height, and one
that brings only a name has the name set as a wordmark — the templates artifact
carries no mark of its own. Figures are written with the locale stated rather than
inherited, because both the grouping of a number and the symbol for a currency code
change with it and a preset that let the JVM decide would render a different sheet on
a different machine. Guarded by a smoke test (including the unknown-mark data error, a
line with no mark, the wordmark fallback, the currency named once and carried, the
uppercased due line, a trunk prefix printed but not dialled, the support contacts as
annotations, a missing ship-to and a missing note block, an empty document, and a
thirty-line invoice that runs on, repeats its column names and numbers its pages), an
exact layout snapshot and a pixel-parity gate; the examples showcase gains
`invoice-payments-v2`.

- **A navy-plate CV preset: `MidnightNavy`.** A one-page sheet on a full-height navy
plate: an outlined monogram over a two-weight name and a tracked role line, then
contact, education, metered skills and dotted languages down the plate, beside a
Expand Down
Binary file added assets/readme/examples/invoice-payments-v2.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
import com.demcha.examples.templates.invoice.ClassicInvoiceV2Example;
import com.demcha.examples.templates.invoice.ConsultingInvoiceV2Example;
import com.demcha.examples.templates.invoice.LumaStudioInvoiceV2Example;
import com.demcha.examples.templates.invoice.PaymentsInvoiceV2Example;
import com.demcha.examples.templates.invoice.InvoiceCinematicFileExample;
import com.demcha.examples.templates.invoice.ModernInvoiceV2Example;
import com.demcha.examples.templates.proposal.CinematicProposalFileExample;
Expand Down Expand Up @@ -190,6 +191,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + ClassicInvoiceV2Example.generate());
System.out.println("Generated: " + ConsultingInvoiceV2Example.generate());
System.out.println("Generated: " + LumaStudioInvoiceV2Example.generate());
System.out.println("Generated: " + PaymentsInvoiceV2Example.generate());

// Proposals
System.out.println("Generated: " + ProposalCinematicFileExample.generate());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.demcha.examples.support;

import com.demcha.compose.document.templates.data.invoice.InvoiceBrand;
import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock;
import com.demcha.compose.document.templates.data.invoice.InvoiceMasthead;
import com.demcha.compose.document.templates.data.invoice.InvoiceNotesBlock;
import com.demcha.compose.document.templates.data.invoice.InvoicePaymentBlock;
import com.demcha.compose.document.templates.data.invoice.InvoiceRecipient;
import com.demcha.compose.document.templates.data.invoice.InvoiceServiceLines;
import com.demcha.compose.document.templates.data.invoice.InvoiceSummaryBlock;
import com.demcha.compose.document.templates.data.invoice.InvoiceTotalsBlock;
import com.demcha.compose.document.templates.data.invoice.StructuredInvoiceData;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
* Shared sample data for the Payments invoice example.
*
* <p>Kept in lockstep with the qa module's {@code PaymentsInvoiceFixtures} — the
* two modules cannot share a source file, so a content change here belongs
* there too.</p>
*/
public final class PaymentsInvoiceSampleData {

private PaymentsInvoiceSampleData() {
}

/**
* The sample invoice — seven metadata rows, two addressed parties, six
* marked service lines, seven payment fields, two summed rows and a
* two-line note.
*
* @return the document the example renders
*/
public static StructuredInvoiceData sample() {
return new StructuredInvoiceData(
brand(), supplier(), masthead(), billTo(), shipTo(),
new InvoiceSummaryBlock("", "", ""),
serviceLines(), totals(), payment(), notes(), "GBP");
}

/** No logo: the wordmark is what a document without one falls back to. */
private static InvoiceBrand brand() {
return new InvoiceBrand(null, "Meridian", "", "", "", "");
}

private static InvoiceContactBlock supplier() {
return new InvoiceContactBlock(
"Meridian Payments Ltd.",
List.of("1 Harbour Exchange", "Canary Wharf", "London E14 9GE",
"United Kingdom"),
"", "", "",
"", "Company No. 09048900",
"", "VAT No. GB 123 4567 89");
}

private static InvoiceMasthead masthead() {
List<InvoiceMasthead.Entry> entries = new ArrayList<>();
entries.add(new InvoiceMasthead.Entry("Invoice Number", "INV-2025-05-10472", false));
entries.add(new InvoiceMasthead.Entry("Invoice Date", "10 May 2025", false));
entries.add(new InvoiceMasthead.Entry("Billing Period", "1 - 30 April 2025", false));
entries.add(new InvoiceMasthead.Entry("Account ID", "acct_4Q8xTb2mKp", false));
entries.add(new InvoiceMasthead.Entry("Payment Terms", "Net 30", false));
entries.add(new InvoiceMasthead.Entry("Due Date", "14 June 2025", false));
entries.add(new InvoiceMasthead.Entry("Status", "UNPAID", true));
return new InvoiceMasthead("INVOICE", entries);
}

/** The subline is where a billed party's registration goes on this sheet. */
private static InvoiceRecipient billTo() {
return new InvoiceRecipient("BILL TO", "Northwind Ltd.",
"VAT No. GB 987 6543 21",
List.of("Attention: Finance Team", "42 Bridgewater Street",
"Manchester M15 4QT", "United Kingdom"),
"", "");
}

private static InvoiceRecipient shipTo() {
return new InvoiceRecipient("SHIP TO", "Northwind Ltd.", "",
List.of("Operations Centre", "7 Kingsway Park",
"Leeds LS12 6BD", "United Kingdom"),
"", "");
}

private static InvoiceServiceLines serviceLines() {
InvoiceServiceLines.Columns columns = new InvoiceServiceLines.Columns(
"", "DESCRIPTION", "", "QUANTITY", "UNIT PRICE", "AMOUNT", "VAT");
List<InvoiceServiceLines.Line> lines = new ArrayList<>();
lines.add(line(1, "Subscription Billing",
"Subscription management and recurring billing",
"1", "250.00", "250.00", "20%", "card"));
lines.add(line(2, "Payment Processing",
"Online card payments processing (volume tier)",
"1", "1,180.00", "1,180.00", "20%", "card-settings"));
lines.add(line(3, "Fraud Screening",
"Advanced fraud screening and rules",
"1", "320.00", "320.00", "20%", "shield"));
lines.add(line(4, "Company Formation",
"Incorporation and compliance filings",
"1", "180.00", "180.00", "20%", "globe"));
lines.add(line(5, "In-Person Terminals",
"Terminal rental and firmware updates",
"4", "39.00", "156.00", "20%", "mobile"));
lines.add(line(6, "Priority Support",
"Named engineer, one-hour response",
"1", "100.00", "100.00", "20%", "headset"));
return new InvoiceServiceLines(columns, lines);
}

private static InvoiceServiceLines.Line line(int number, String title, String description,
String quantity, String unitPrice,
String amount, String vat, String icon) {
return new InvoiceServiceLines.Line(number, title, description, "",
decimal(quantity), "", decimal(unitPrice), decimal(amount), vat, icon);
}

private static InvoiceTotalsBlock totals() {
List<InvoiceTotalsBlock.Row> rows = new ArrayList<>();
rows.add(new InvoiceTotalsBlock.Row("Subtotal (excl. VAT)", decimal("2,186.00")));
rows.add(new InvoiceTotalsBlock.Row("VAT at 20%", decimal("437.20")));
return new InvoiceTotalsBlock(rows, "TOTAL DUE", decimal("2,623.20"));
}

private static InvoicePaymentBlock payment() {
List<InvoicePaymentBlock.Field> fields = new ArrayList<>();
fields.add(new InvoicePaymentBlock.Field("Bank Name", "Harbour Bank plc"));
fields.add(new InvoicePaymentBlock.Field("Account Name", "Meridian Payments Ltd."));
fields.add(new InvoicePaymentBlock.Field("Sort Code", "40-02-50"));
fields.add(new InvoicePaymentBlock.Field("Account Number", "71295408"));
fields.add(new InvoicePaymentBlock.Field("IBAN", "GB29 HRBK 4002 5071 2954 08"));
fields.add(new InvoicePaymentBlock.Field("SWIFT / BIC", "HRBKGB2L"));
fields.add(new InvoicePaymentBlock.Field("Reference", "INV-2025-05-10472"));
return new InvoicePaymentBlock("PAYMENT DETAILS", fields,
"Please include the invoice number as payment reference.",
"Due by 14 June 2025",
"Payment due by 14 June 2025",
"",
"Questions? We're here to help.");
}

private static InvoiceNotesBlock notes() {
return new InvoiceNotesBlock("NOTES",
List.of("Thank you for your business. This invoice covers services and "
+ "fees for the period stated above.",
"Late payment may incur interest at the statutory rate."),
"support@meridianpayments.example",
"+44 (0) 20 3966 1900");
}

/** A printed figure back to a number: the grouping comes off. */
private static BigDecimal decimal(String printed) {
return new BigDecimal(printed.replace(",", ""));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ record Entry(String title, String description, List<String> tags, String codeUrl
invoice("invoice-cinematic", "InvoiceCinematicFileExample", "Cinematic Invoice", "Layered ModernInvoice preset with theme-driven layout, advanced tables, and totals.", "invoice", "cinematic");
invoice("invoice-consulting-v2", "v2/ConsultingInvoiceV2Example", "Consulting Invoice", "The ConsultingInvoice preset on the structured invoice model — brand lockup with the caller's logo, labelled masthead metadata, priced service lines with service periods, a totals stack and bank payment fields.", "invoice");
invoice("invoice-luma-studio-v2", "v2/LumaStudioInvoiceV2Example", "Luma Studio Invoice", "The LumaStudioInvoice preset on the structured invoice model — a cream sidebar carrying the brand lockup and its ornament, a billed-to / shipped-to pair, priced service lines with a VAT column, and the notes and bank details above a sign-off band.", "invoice");
invoice("invoice-payments-v2", "v2/PaymentsInvoiceV2Example", "Payments Invoice", "The PaymentsInvoice preset on the structured invoice model — a diagonal band crossing the masthead, a half-split issuer and metadata header, two addressed parties, marked service lines that repeat their header across pages, and a settlement row pairing bank details against the totals.", "invoice");
invoice("invoice-modern-v2", "v2/ModernInvoiceV2Example", "Modern Invoice", "The ModernInvoice preset composed straight from an InvoiceDocumentSpec — line items, totals and payment block driven by the BrandTheme rather than per-document styling.", "invoice");
invoice("invoice-classic-v2", "v2/ClassicInvoiceV2Example", "Classic Invoice", "The ClassicInvoice preset — letterhead header band, TOTAL DUE hero strip, BILL TO / FROM columns, and a dedicated Summary table after the line items.", "invoice");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.demcha.examples.templates.invoice;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.data.invoice.StructuredInvoiceData;
import com.demcha.compose.document.templates.invoice.presets.PaymentsInvoice;
import com.demcha.examples.support.ExampleOutputPaths;
import com.demcha.examples.support.PaymentsInvoiceSampleData;

import java.nio.file.Path;

/**
* Renders the layered {@code invoice.v2} Payments preset against the design
* sample.
*
* <p>Output:
* {@code examples/target/generated-pdfs/templates/invoice/invoice-payments-v2.pdf}.</p>
*
* <p>The preset owns its page geometry and its pagination — the table's header
* repeats and a continuation page reserves a deeper bottom margin — so the
* session starts unconfigured. The sample brings no logo, so the lockup box
* beside the title is filled with the brand's name as a wordmark.</p>
*/
public final class PaymentsInvoiceV2Example {

private PaymentsInvoiceV2Example() {
}

/**
* @return absolute path of the rendered PDF
* @throws Exception if rendering fails
*/
public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare("templates/invoice",
"invoice-payments-v2.pdf");
StructuredInvoiceData data = PaymentsInvoiceSampleData.sample();
DocumentTemplate<StructuredInvoiceData> template = PaymentsInvoice.create();

try (DocumentSession document = GraphCompose.document(outputFile).create()) {
template.compose(document, data);
document.buildPdf();
}
return outputFile;
}

/**
* @param args ignored
* @throws Exception if rendering fails
*/
public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
Loading
Loading