From 71a7cb9a4f69e4f59338166429058438003642d8 Mon Sep 17 00:00:00 2001
From: DemchaAV The theme assembled below ("Studio Emerald") is a deliberately
- * distinctive identity — emerald primary + copper accent on a warm
- * ivory paper — so when you open the generated invoice next to
- * {@link InvoiceCinematicFileExample} the difference is unmistakable.
- * The point is not the colour scheme itself but the demonstration that
- * every visible token on the invoice is driven from the theme
- * record: change a colour or font here and the invoice updates
- * without touching the template code. Use this example as a starter when you need to brand GraphCompose
- * output for your own project: copy the body of
- * {@link #studioEmeraldTheme()}, tweak the seven palette colours and
- * the seven text-scale styles, and you have a custom theme that any
- * {@code *TemplateV2} class can consume. The theme overrides every token rather than deriving from
- * {@link BusinessTheme#modern()} so the example shows the full
- * surface area a brand-customising consumer needs to fill in. Responsibility: define one invoice scene that translates business
- * data into semantic sections, rows, and totals inside a live
- * {@link DocumentSession}. Responsibility: define one reusable proposal scene that emits
- * semantic proposal structure into a caller-owned
- * {@link DocumentSession}. The template demonstrates the v1.4 / v1.5 cinematic primitives
- * stacked on top of one another: Every visual style is derived from the {@link BusinessTheme}
- * passed to the constructor — palette, text scale, stroke colour. The
- * default constructor picks {@link BusinessTheme#modern()}; pass any
- * other theme (or a custom one) to render the same
- * {@link InvoiceDocumentSpec} in a different look without rewriting
- * the composition. Stacks the v1.4 / v1.5 cinematic primitives: Constructed from any {@link BusinessTheme} so the same proposal
- * data can ship through {@code BusinessTheme.classic()},
- * {@code BusinessTheme.modern()}, or {@code BusinessTheme.executive()}
- * without touching the composition code. Reuses the invoice palette + spacing tokens — the two families
* share one modern business look; a future cleanup may rename those
diff --git a/src/main/java/com/demcha/compose/document/templates/data/invoice/package-info.java b/src/main/java/com/demcha/compose/document/templates/data/invoice/package-info.java
index f1be624f0..c12007da6 100644
--- a/src/main/java/com/demcha/compose/document/templates/data/invoice/package-info.java
+++ b/src/main/java/com/demcha/compose/document/templates/data/invoice/package-info.java
@@ -1,6 +1,5 @@
/**
* Shared, render-neutral invoice document specs and supporting data
- * records, consumed by both the cinematic builtin {@code InvoiceTemplateV2}
- * and the layered {@code invoice.v2} presets.
+ * records, consumed by the layered {@code invoice.v2} presets.
*/
package com.demcha.compose.document.templates.data.invoice;
diff --git a/src/main/java/com/demcha/compose/document/templates/data/proposal/package-info.java b/src/main/java/com/demcha/compose/document/templates/data/proposal/package-info.java
index 70122c60a..aacbda968 100644
--- a/src/main/java/com/demcha/compose/document/templates/data/proposal/package-info.java
+++ b/src/main/java/com/demcha/compose/document/templates/data/proposal/package-info.java
@@ -1,6 +1,5 @@
/**
* Shared, render-neutral proposal document specs and supporting data
- * records, consumed by both the cinematic builtin {@code ProposalTemplateV2}
- * and the layered {@code proposal.v2} presets.
+ * records, consumed by the layered {@code proposal.v2} presets.
*/
package com.demcha.compose.document.templates.data.proposal;
diff --git a/src/main/java/com/demcha/compose/document/templates/invoice/v2/presets/ModernInvoice.java b/src/main/java/com/demcha/compose/document/templates/invoice/v2/presets/ModernInvoice.java
index 99442667b..4afe5ec25 100644
--- a/src/main/java/com/demcha/compose/document/templates/invoice/v2/presets/ModernInvoice.java
+++ b/src/main/java/com/demcha/compose/document/templates/invoice/v2/presets/ModernInvoice.java
@@ -30,12 +30,11 @@
* {@code create(BrandTheme)} factory returns a thin
* {@link DocumentTemplate} whose {@code compose} sequences a hero panel,
* the FROM / BILL TO parties, the line-items table, and a notes /
- * payment-terms footer. The visual intent is ported from the cinematic
- * {@code builtins.InvoiceTemplateV2}; the hero, party labels, table
- * header, totals, and footer read their colours / fonts / sizes from the
- * theme (replacing the {@code BusinessTheme} the builtin used). The
- * line-item body cells intentionally inherit the DSL default table-cell
- * text to stay a pixel match for the builtin — see {@code compose}.
seam on a BrandTheme. With the builtins gone, the per-family api interfaces
InvoiceTemplate / ProposalTemplate have no implementer.
Delete the two V2 builtins, the InvoiceTemplate / ProposalTemplate
interfaces, their unit and demo tests, and CustomBusinessThemeExample (it
demoed custom theming the v2 invoice does not yet express). Drop the
CustomBusinessTheme example registrations and repoint the surviving javadoc
and showcase strings off the removed types. The visual parity tests keep
rendering ModernInvoice / ModernProposal against their committed baselines.
The builtins' private composer substrate (support/business) is removed in a
follow-up.
Tests: ./mvnw verify javadoc:javadoc -pl . — 1602 tests, 0 failures,
javadoc clean; examples test-compile; benchmark gates + perf-smoke green.
---
examples/README.md | 21 --
.../demcha/examples/GenerateAllExamples.java | 2 -
.../themes/CustomBusinessThemeExample.java | 166 ---------
.../flagships/MasterShowcaseExample.java | 8 +-
.../examples/support/ShowcaseMetadata.java | 3 -
.../templates/api/InvoiceTemplate.java | 62 ----
.../templates/api/ProposalTemplate.java | 62 ----
.../templates/builtins/InvoiceTemplateV2.java | 304 ----------------
.../builtins/ProposalTemplateV2.java | 325 ------------------
.../templates/builtins/package-info.java | 4 -
.../templates/core/theme/BrandTheme.java | 4 +-
.../templates/data/invoice/package-info.java | 3 +-
.../templates/data/proposal/package-info.java | 3 +-
.../invoice/v2/presets/ModernInvoice.java | 11 +-
.../proposal/v2/presets/ModernProposal.java | 20 +-
.../builtins/InvoiceTemplateV2Test.java | 133 -------
.../builtins/ProposalTemplateV2Test.java | 132 -------
.../v2/presets/InvoiceV2VisualParityTest.java | 2 +-
.../presets/ProposalV2VisualParityTest.java | 2 +-
.../visual/CustomBusinessThemeDemoTest.java | 153 ---------
.../visual/InvoiceTemplateV2DemoTest.java | 92 -----
.../visual/ProposalTemplateV2DemoTest.java | 97 ------
22 files changed, 23 insertions(+), 1586 deletions(-)
delete mode 100644 examples/src/main/java/com/demcha/examples/features/themes/CustomBusinessThemeExample.java
delete mode 100644 src/main/java/com/demcha/compose/document/templates/api/InvoiceTemplate.java
delete mode 100644 src/main/java/com/demcha/compose/document/templates/api/ProposalTemplate.java
delete mode 100644 src/main/java/com/demcha/compose/document/templates/builtins/InvoiceTemplateV2.java
delete mode 100644 src/main/java/com/demcha/compose/document/templates/builtins/ProposalTemplateV2.java
delete mode 100644 src/main/java/com/demcha/compose/document/templates/builtins/package-info.java
delete mode 100644 src/test/java/com/demcha/compose/document/templates/builtins/InvoiceTemplateV2Test.java
delete mode 100644 src/test/java/com/demcha/compose/document/templates/builtins/ProposalTemplateV2Test.java
delete mode 100644 src/test/java/com/demcha/testing/visual/CustomBusinessThemeDemoTest.java
delete mode 100644 src/test/java/com/demcha/testing/visual/InvoiceTemplateV2DemoTest.java
delete mode 100644 src/test/java/com/demcha/testing/visual/ProposalTemplateV2DemoTest.java
diff --git a/examples/README.md b/examples/README.md
index ef5a56285..b4af78ec2 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -91,7 +91,6 @@ are with the canonical DSL, then jump to its detailed section below.
| [CV — template gallery](#cv-template-gallery) | The v2 CV presets in one orchestrated run | [Source](src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java) |
| [Cover letter — template gallery](#cover-letter-template-gallery) | All paired v2 cover-letter presets in one orchestrated run | [Source](src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java) |
| [Proposal — cinematic V2](#proposal-cinematic-v2) | `ProposalTemplateV2 + BusinessTheme.modern()` | [PDF](../assets/readme/examples/proposal-cinematic.pdf) · [Source](src/main/java/com/demcha/examples/templates/proposal/ProposalCinematicFileExample.java) |
-| [Custom Business Theme](#custom-business-theme) | Hand-built `BusinessTheme` driving `InvoiceTemplateV2` — theme-token customisation entry point | [PDF](../assets/readme/examples/invoice-custom-theme.pdf) · [Source](src/main/java/com/demcha/examples/features/themes/CustomBusinessThemeExample.java) |
### 🔧 Advanced SPI
@@ -536,26 +535,6 @@ table.columns(...)
[📄 View PDF](../assets/readme/examples/table-advanced.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/features/tables/TableAdvancedExample.java)
-### Custom Business Theme
-
-Build a `BusinessTheme` from raw `DocumentPalette` / `SpacingScale` /
-`TextScale` / `TablePreset` records — no factory shortcut. Plug it
-straight into `InvoiceTemplateV2` to retheme the whole template
-without touching any code that uses it.
-
-```java
-BusinessTheme studioEmerald = new BusinessTheme(
- new DocumentPalette(/* page, surface, surfaceMuted, ink, accent, … */),
- SpacingScale.cinematic(),
- new TextScale(/* h1, h2, body, caption fonts … */),
- TablePreset.cinematic());
-
-new InvoiceTemplateV2(studioEmerald).compose(document, invoice);
-```
-
-[📄 View PDF](../assets/readme/examples/invoice-custom-theme.pdf) ·
-[📜 Full source](src/main/java/com/demcha/examples/features/themes/CustomBusinessThemeExample.java)
-
---
## Public-API surface
diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
index 32b9a2aa5..e93b9a2e2 100644
--- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
+++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java
@@ -39,7 +39,6 @@
import com.demcha.examples.features.structure.MultiSectionExample;
import com.demcha.examples.features.text.RichTextShowcaseExample;
import com.demcha.examples.features.text.SectionPresetsExample;
-import com.demcha.examples.features.themes.CustomBusinessThemeExample;
import com.demcha.examples.features.title.BookTemplateExample;
import com.demcha.examples.features.transforms.TransformsExample;
import com.demcha.examples.flagships.BusinessReportExample;
@@ -186,7 +185,6 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + MultiSectionExample.generate());
// Theming + chrome
- System.out.println("Generated: " + CustomBusinessThemeExample.generate());
System.out.println("Generated: " + PdfChromeExample.generate());
System.out.println("Generated: " + PageNumberingExample.generate());
System.out.println("Generated: " + ViewerPreferencesExample.generate());
diff --git a/examples/src/main/java/com/demcha/examples/features/themes/CustomBusinessThemeExample.java b/examples/src/main/java/com/demcha/examples/features/themes/CustomBusinessThemeExample.java
deleted file mode 100644
index 0c4d18a67..000000000
--- a/examples/src/main/java/com/demcha/examples/features/themes/CustomBusinessThemeExample.java
+++ /dev/null
@@ -1,166 +0,0 @@
-package com.demcha.examples.features.themes;
-
-import com.demcha.compose.GraphCompose;
-import com.demcha.compose.document.api.DocumentPageSize;
-import com.demcha.compose.document.api.DocumentSession;
-import com.demcha.compose.document.style.DocumentColor;
-import com.demcha.compose.document.style.DocumentInsets;
-import com.demcha.compose.document.style.DocumentStroke;
-import com.demcha.compose.document.style.DocumentTextDecoration;
-import com.demcha.compose.document.style.DocumentTextStyle;
-import com.demcha.compose.document.table.DocumentTableStyle;
-import com.demcha.compose.document.templates.builtins.InvoiceTemplateV2;
-import com.demcha.compose.document.theme.BusinessTheme;
-import com.demcha.compose.document.theme.DocumentPalette;
-import com.demcha.compose.document.theme.SpacingScale;
-import com.demcha.compose.document.theme.TablePreset;
-import com.demcha.compose.document.theme.TextScale;
-import com.demcha.compose.font.FontName;
-import com.demcha.examples.support.ExampleDataFactory;
-import com.demcha.examples.support.ExampleOutputPaths;
-
-import java.awt.Color;
-import java.nio.file.Path;
-
-/**
- * Phase E.4 — runnable showcase that hand-builds a {@link BusinessTheme}
- * from scratch (rather than picking one of the {@code classic / modern
- * / executive} presets) and pipes it through {@link InvoiceTemplateV2}.
- *
- * {@code
- * InvoiceTemplate template = new InvoiceTemplateV2();
- * InvoiceDocumentSpec invoice = InvoiceDocumentSpec.builder()
- * .invoiceNumber("GC-2026-041")
- * .fromParty(party -> party.name("GraphCompose Studio"))
- * .billToParty(party -> party.name("Northwind Systems"))
- * .lineItem("Template architecture", "Reusable business document flow", "1", "GBP 980", "GBP 980")
- * .totalRow("Total", "GBP 980")
- * .build();
- *
- * try (DocumentSession document = GraphCompose.document(Path.of("invoice.pdf")).create()) {
- * template.compose(document, invoice);
- * document.buildPdf();
- * }
- * }
- */
-public interface InvoiceTemplate {
-
- /**
- * Stable public template identifier.
- *
- * @return unique template id used by registries and integrations
- */
- String getTemplateId();
-
- /**
- * Human-readable display name.
- *
- * @return template display name
- */
- String getTemplateName();
-
- /**
- * Optional human-readable description.
- *
- * @return template description, or an empty string when omitted
- */
- default String getDescription() {
- return "";
- }
-
- /**
- * Composes an invoice into a live document session.
- *
- * @param document active mutable document session receiving template nodes
- * @param spec invoice document spec
- * @throws NullPointerException if an implementation requires non-null inputs
- */
- void compose(DocumentSession document, InvoiceDocumentSpec spec);
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/api/ProposalTemplate.java b/src/main/java/com/demcha/compose/document/templates/api/ProposalTemplate.java
deleted file mode 100644
index 92d153c5a..000000000
--- a/src/main/java/com/demcha/compose/document/templates/api/ProposalTemplate.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.demcha.compose.document.templates.api;
-
-import com.demcha.compose.document.api.DocumentSession;
-import com.demcha.compose.document.templates.data.proposal.ProposalDocumentSpec;
-
-/**
- * Canonical compose contract for reusable proposal templates.
- *
- * {@code
- * ProposalTemplate template = new ProposalTemplateV2();
- * ProposalDocumentSpec proposal = ProposalDocumentSpec.builder()
- * .projectTitle("GraphCompose rollout")
- * .section("Scope", "Introduce reusable invoice and proposal templates.")
- * .timelineItem("Week 1", "5 days", "Foundation and review loop")
- * .pricingRow("Delivery", "Template implementation", "GBP 4,450")
- * .emphasizedPricingRow("Total", "Fixed-price delivery", "GBP 4,450")
- * .build();
- *
- * try (DocumentSession document = GraphCompose.document(Path.of("proposal.pdf")).create()) {
- * template.compose(document, proposal);
- * document.buildPdf();
- * }
- * }
- */
-public interface ProposalTemplate {
-
- /**
- * Stable public template identifier.
- *
- * @return unique template id used by registries and integrations
- */
- String getTemplateId();
-
- /**
- * Human-readable display name.
- *
- * @return template display name
- */
- String getTemplateName();
-
- /**
- * Optional human-readable description.
- *
- * @return template description, or an empty string when omitted
- */
- default String getDescription() {
- return "";
- }
-
- /**
- * Composes a proposal into a live document session.
- *
- * @param document active mutable document session receiving template nodes
- * @param spec proposal document spec
- * @throws NullPointerException if an implementation requires non-null inputs
- */
- void compose(DocumentSession document, ProposalDocumentSpec spec);
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/builtins/InvoiceTemplateV2.java b/src/main/java/com/demcha/compose/document/templates/builtins/InvoiceTemplateV2.java
deleted file mode 100644
index 87fc8894a..000000000
--- a/src/main/java/com/demcha/compose/document/templates/builtins/InvoiceTemplateV2.java
+++ /dev/null
@@ -1,304 +0,0 @@
-package com.demcha.compose.document.templates.builtins;
-
-import com.demcha.compose.document.api.DocumentSession;
-import com.demcha.compose.document.dsl.TableBuilder;
-import com.demcha.compose.document.style.*;
-import com.demcha.compose.document.table.DocumentTableColumn;
-import com.demcha.compose.document.table.DocumentTableStyle;
-import com.demcha.compose.document.templates.api.InvoiceTemplate;
-import com.demcha.compose.document.templates.data.invoice.*;
-import com.demcha.compose.document.templates.support.common.TemplateLifecycleLog;
-import com.demcha.compose.document.theme.BusinessTheme;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Phase E.1 — cinematic invoice template that composes against the
- * canonical DSL using a {@link BusinessTheme} for every visual choice.
- *
- *
- *
- *
- *
- *
- *
- *
Why the parties render inline rather than through * {@code core.identity.PartyIdentity}: an invoice carries two diff --git a/src/main/java/com/demcha/compose/document/templates/proposal/v2/presets/ModernProposal.java b/src/main/java/com/demcha/compose/document/templates/proposal/v2/presets/ModernProposal.java index 2dd917c9d..45f064c4a 100644 --- a/src/main/java/com/demcha/compose/document/templates/proposal/v2/presets/ModernProposal.java +++ b/src/main/java/com/demcha/compose/document/templates/proposal/v2/presets/ModernProposal.java @@ -32,13 +32,11 @@ * {@link DocumentTemplate} whose {@code compose} sequences a hero panel, * an executive-summary panel, the FROM / TO parties, the body sections, * the timeline + pricing tables, the acceptance terms, and a footer. The - * visual intent is ported from the cinematic - * {@code builtins.ProposalTemplateV2}; the hero, summary, party labels, - * section headings, table headers, totals, and footer read their colours - * / fonts / sizes from the theme (replacing the {@code BusinessTheme} the - * builtin used). The table body cells intentionally inherit the DSL - * default table-cell text to stay a pixel match for the builtin — see - * {@code compose}.
+ * visual intent is the cinematic "modern business" proposal look, fully + * theme-driven: the hero, summary, party labels, section headings, table + * headers, totals, and footer read their colours / fonts / sizes from the + * {@code BrandTheme}. The table body cells intentionally inherit the DSL + * default table-cell text — see {@code compose}. * *Why the parties render inline rather than through * {@code core.identity.PartyIdentity}: a proposal carries two @@ -213,11 +211,9 @@ public void compose(DocumentSession document, ProposalDocumentSpec spec) { data.sender(), labelStyle, bodyStyle)) .addSection("ProposalRecipient", col -> partyBlock(col, "TO", data.recipient(), labelStyle, bodyStyle))) - // NB: the cinematic builtin ProposalTemplateV2 omits this build() - // on its first page-flow, so its hero / summary / parties never - // render — a latent bug. The layered preset fixes it: build the - // root flow so the title, executive summary, and FROM/TO parties - // are emitted before the section/timeline/pricing flows below. + // Build the root page-flow so the title, executive summary, and + // FROM/TO parties are emitted before the section/timeline/pricing + // flows below — without this build() the root flow's content drops. .build(); for (ProposalSection section : data.sections()) { diff --git a/src/test/java/com/demcha/compose/document/templates/builtins/InvoiceTemplateV2Test.java b/src/test/java/com/demcha/compose/document/templates/builtins/InvoiceTemplateV2Test.java deleted file mode 100644 index f80d6021f..000000000 --- a/src/test/java/com/demcha/compose/document/templates/builtins/InvoiceTemplateV2Test.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.demcha.compose.document.templates.builtins; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.api.DocumentSession; -import com.demcha.compose.document.layout.LayoutGraph; -import com.demcha.compose.document.style.DocumentInsets; -import com.demcha.compose.document.templates.api.InvoiceTemplate; -import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; -import com.demcha.compose.document.theme.BusinessTheme; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Phase E.1 — InvoiceTemplateV2 must compose against the canonical DSL, - * accept any BusinessTheme via the constructor, and produce a valid - * paginated PDF for the standard {@link InvoiceDocumentSpec} sample. - * - * @author Artem Demchyshyn - */ -class InvoiceTemplateV2Test { - - @Test - void defaultConstructorUsesModernTheme() { - InvoiceTemplateV2 template = new InvoiceTemplateV2(); - assertThat(template.getTemplateId()).isEqualTo("invoice-v2"); - assertThat(template.getTemplateName()).isEqualTo("Invoice V2 (cinematic)"); - } - - @Test - void invoiceTemplateImplementsCanonicalInterface() { - InvoiceTemplate template = new InvoiceTemplateV2(); - assertThat(template).isNotNull(); - } - - @Test - void composeProducesValidPdfBytesForSampleInvoice() throws Exception { - InvoiceDocumentSpec spec = sampleInvoice(); - InvoiceTemplateV2 template = new InvoiceTemplateV2(); - - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .pageBackground(BusinessTheme.modern().pageBackground()) - .margin(DocumentInsets.of(28)) - .create()) { - - template.compose(document, spec); - byte[] bytes = document.toPdfBytes(); - assertThat(bytes).isNotEmpty(); - assertThat(new String(bytes, 0, 5, java.nio.charset.StandardCharsets.US_ASCII)) - .as("PDF magic header — graphics-state leak would corrupt this") - .isEqualTo("%PDF-"); - } - } - - @Test - void differentThemesProduceDistinctRenderedBytes() throws Exception { - InvoiceDocumentSpec spec = sampleInvoice(); - - byte[] modernBytes = renderWithTheme(spec, BusinessTheme.modern()); - byte[] classicBytes = renderWithTheme(spec, BusinessTheme.classic()); - - // Both must be valid PDF byte streams. - assertThat(modernBytes).startsWith("%PDF-".getBytes()); - assertThat(classicBytes).startsWith("%PDF-".getBytes()); - // The themed variants should differ in bytes — the palette colours - // get embedded in the PDF stream, so a theme switch must be - // observable downstream. - assertThat(modernBytes).isNotEqualTo(classicBytes); - } - - @Test - void layoutGraphContainsLineItemsTable() throws Exception { - InvoiceDocumentSpec spec = sampleInvoice(); - InvoiceTemplateV2 template = new InvoiceTemplateV2(); - - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .margin(DocumentInsets.of(28)) - .create()) { - - template.compose(document, spec); - LayoutGraph graph = document.layoutGraph(); - assertThat(graph.totalPages()).isGreaterThanOrEqualTo(1); - // The layout graph should contain at least one node whose - // semantic name signals the line-items table — this is the - // anchor invariant that proves the template ran the table - // composition (not just the hero block). - assertThat(graph.nodes()) - .anyMatch(node -> node.semanticName() != null - && node.semanticName().contains("InvoiceLineItems")); - } - } - - private static byte[] renderWithTheme(InvoiceDocumentSpec spec, BusinessTheme theme) throws Exception { - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .pageBackground(theme.pageBackground()) - .margin(DocumentInsets.of(28)) - .create()) { - new InvoiceTemplateV2(theme).compose(document, spec); - return document.toPdfBytes(); - } - } - - private static InvoiceDocumentSpec sampleInvoice() { - return InvoiceDocumentSpec.builder() - .title("Invoice") - .invoiceNumber("GC-2026-041") - .issueDate("02 Apr 2026") - .dueDate("16 Apr 2026") - .reference("Platform Refresh Sprint") - .status("Pending") - .fromParty(party -> party - .name("GraphCompose Studio") - .addressLines("18 Layout Street", "London, UK", "EC1A 4GC") - .email("billing@graphcompose.dev")) - .billToParty(party -> party - .name("Northwind Systems") - .addressLines("Attn: Finance Team", "410 Market Avenue", "Manchester, UK") - .email("ap@northwind.example")) - .lineItem("Discovery workshop", "Stakeholder interviews", "1", "GBP 1,450", "GBP 1,450") - .lineItem("Template architecture", "Reusable document flows", "2", "GBP 980", "GBP 1,960") - .lineItem("Render QA", "Visual validation passes", "3", "GBP 320", "GBP 960") - .summaryRow("Subtotal", "GBP 4,370") - .summaryRow("VAT (20%)", "GBP 874") - .totalRow("Total", "GBP 5,244") - .note("Please include the invoice number on your remittance advice.") - .paymentTerm("Payment due within 14 calendar days.") - .footerNote("Thank you for choosing GraphCompose for production rendering.") - .build(); - } -} diff --git a/src/test/java/com/demcha/compose/document/templates/builtins/ProposalTemplateV2Test.java b/src/test/java/com/demcha/compose/document/templates/builtins/ProposalTemplateV2Test.java deleted file mode 100644 index 8e07bec7a..000000000 --- a/src/test/java/com/demcha/compose/document/templates/builtins/ProposalTemplateV2Test.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.demcha.compose.document.templates.builtins; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.api.DocumentSession; -import com.demcha.compose.document.layout.LayoutGraph; -import com.demcha.compose.document.style.DocumentInsets; -import com.demcha.compose.document.templates.api.ProposalTemplate; -import com.demcha.compose.document.templates.data.proposal.ProposalDocumentSpec; -import com.demcha.compose.document.theme.BusinessTheme; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Phase E.2 — ProposalTemplateV2 must compose against the canonical DSL, - * accept any BusinessTheme, and produce a valid PDF for the standard - * sample proposal. - * - * @author Artem Demchyshyn - */ -class ProposalTemplateV2Test { - - @Test - void defaultConstructorPicksModernTheme() { - ProposalTemplateV2 template = new ProposalTemplateV2(); - assertThat(template.getTemplateId()).isEqualTo("proposal-v2"); - assertThat(template.getTemplateName()).isEqualTo("Proposal V2 (cinematic)"); - } - - @Test - void implementsCanonicalProposalTemplateInterface() { - ProposalTemplate template = new ProposalTemplateV2(); - assertThat(template).isNotNull(); - } - - @Test - void composeProducesValidPdfBytesForSampleProposal() throws Exception { - ProposalDocumentSpec spec = sampleProposal(); - ProposalTemplateV2 template = new ProposalTemplateV2(); - - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .pageBackground(BusinessTheme.modern().pageBackground()) - .margin(DocumentInsets.of(28)) - .create()) { - - template.compose(document, spec); - byte[] bytes = document.toPdfBytes(); - assertThat(bytes).isNotEmpty(); - assertThat(new String(bytes, 0, 5, java.nio.charset.StandardCharsets.US_ASCII)) - .as("PDF magic header — graphics-state leak corrupts this") - .isEqualTo("%PDF-"); - } - } - - @Test - void differentThemesProduceDistinctRenderedBytes() throws Exception { - ProposalDocumentSpec spec = sampleProposal(); - - byte[] modernBytes = renderWithTheme(spec, BusinessTheme.modern()); - byte[] classicBytes = renderWithTheme(spec, BusinessTheme.classic()); - - assertThat(modernBytes).startsWith("%PDF-".getBytes()); - assertThat(classicBytes).startsWith("%PDF-".getBytes()); - assertThat(modernBytes) - .as("theme switch must be observable downstream — palette colours embed in the PDF stream") - .isNotEqualTo(classicBytes); - } - - @Test - void layoutGraphContainsTimelineAndPricingTables() throws Exception { - ProposalDocumentSpec spec = sampleProposal(); - ProposalTemplateV2 template = new ProposalTemplateV2(); - - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .margin(DocumentInsets.of(28)) - .create()) { - - template.compose(document, spec); - LayoutGraph graph = document.layoutGraph(); - assertThat(graph.totalPages()).isGreaterThanOrEqualTo(1); - assertThat(graph.nodes()) - .as("the timeline table is composed for every proposal that has timeline rows") - .anyMatch(node -> node.semanticName() != null - && node.semanticName().contains("ProposalTimeline")); - assertThat(graph.nodes()) - .as("the pricing table is composed for every proposal that has pricing rows") - .anyMatch(node -> node.semanticName() != null - && node.semanticName().contains("ProposalPricing")); - } - } - - private static byte[] renderWithTheme(ProposalDocumentSpec spec, BusinessTheme theme) throws Exception { - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .pageBackground(theme.pageBackground()) - .margin(DocumentInsets.of(28)) - .create()) { - new ProposalTemplateV2(theme).compose(document, spec); - return document.toPdfBytes(); - } - } - - private static ProposalDocumentSpec sampleProposal() { - return ProposalDocumentSpec.builder() - .title("Proposal") - .proposalNumber("PROP-2026-014") - .preparedDate("02 Apr 2026") - .validUntil("16 Apr 2026") - .projectTitle("GraphCompose rollout") - .executiveSummary("This proposal describes a practical adoption path for reusable GraphCompose templates.") - .sender(party -> party.name("GraphCompose Studio") - .addressLines("18 Layout Street", "London, UK") - .email("hello@graphcompose.dev")) - .recipient(party -> party.name("Northwind Systems") - .addressLines("410 Market Avenue", "Manchester, UK") - .email("platform@northwind.example")) - .section("Scope", - "Introduce built-in invoice and proposal templates with a consistent business presentation layer.") - .section("Deliverables", - "Public DTOs and template interfaces for invoice and proposal rendering.") - .timelineItem("Week 1", "5 days", "Invoice API and first template delivery.") - .timelineItem("Week 2", "5 days", "Proposal layout and review loop.") - .pricingRow("Foundation", "Template APIs and DTO modeling", "GBP 3,200") - .pricingRow("Document delivery", "Invoice and proposal templates", "GBP 4,450") - .emphasizedPricingRow("Total investment", "Fixed-price project delivery", "GBP 9,500") - .acceptanceTerm("Proposal pricing is valid until the stated expiration date.") - .footerNote("Prepared to demonstrate the business-document side of GraphCompose.") - .build(); - } -} diff --git a/src/test/java/com/demcha/compose/document/templates/invoice/v2/presets/InvoiceV2VisualParityTest.java b/src/test/java/com/demcha/compose/document/templates/invoice/v2/presets/InvoiceV2VisualParityTest.java index 61d6faa75..3e04254df 100644 --- a/src/test/java/com/demcha/compose/document/templates/invoice/v2/presets/InvoiceV2VisualParityTest.java +++ b/src/test/java/com/demcha/compose/document/templates/invoice/v2/presets/InvoiceV2VisualParityTest.java @@ -21,7 +21,7 @@ *
Each preset renders the same canonical {@link InvoiceDocumentSpec} * on A4 at the preset's {@code RECOMMENDED_MARGIN}; the PDF is rasterised * page-by-page and compared per-pixel against a checked-in baseline PNG. - * {@code ModernInvoice} reproduces the cinematic {@code InvoiceTemplateV2} + * {@code ModernInvoice} renders the cinematic "modern business" invoice * look on a {@code BrandTheme}, so this gate locks that look against drift.
* *Re-blessing baselines — after a deliberate visual diff --git a/src/test/java/com/demcha/compose/document/templates/proposal/v2/presets/ProposalV2VisualParityTest.java b/src/test/java/com/demcha/compose/document/templates/proposal/v2/presets/ProposalV2VisualParityTest.java index d3f2852e5..20a3dd944 100644 --- a/src/test/java/com/demcha/compose/document/templates/proposal/v2/presets/ProposalV2VisualParityTest.java +++ b/src/test/java/com/demcha/compose/document/templates/proposal/v2/presets/ProposalV2VisualParityTest.java @@ -21,7 +21,7 @@ *
Each preset renders the same canonical {@link ProposalDocumentSpec} * on A4 at the preset's {@code RECOMMENDED_MARGIN}; the PDF is rasterised * page-by-page and compared per-pixel against a checked-in baseline PNG. - * {@code ModernProposal} reproduces the cinematic {@code ProposalTemplateV2} + * {@code ModernProposal} renders the cinematic "modern business" proposal * look on a {@code BrandTheme}, so this gate locks that look against drift.
* *Re-blessing baselines — after a deliberate visual diff --git a/src/test/java/com/demcha/testing/visual/CustomBusinessThemeDemoTest.java b/src/test/java/com/demcha/testing/visual/CustomBusinessThemeDemoTest.java deleted file mode 100644 index 2b81b0ac8..000000000 --- a/src/test/java/com/demcha/testing/visual/CustomBusinessThemeDemoTest.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.demcha.testing.visual; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.api.DocumentSession; -import com.demcha.compose.document.style.DocumentColor; -import com.demcha.compose.document.style.DocumentInsets; -import com.demcha.compose.document.style.DocumentStroke; -import com.demcha.compose.document.style.DocumentTextDecoration; -import com.demcha.compose.document.style.DocumentTextStyle; -import com.demcha.compose.document.table.DocumentTableStyle; -import com.demcha.compose.document.templates.builtins.InvoiceTemplateV2; -import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; -import com.demcha.compose.document.theme.BusinessTheme; -import com.demcha.compose.document.theme.DocumentPalette; -import com.demcha.compose.document.theme.SpacingScale; -import com.demcha.compose.document.theme.TablePreset; -import com.demcha.compose.document.theme.TextScale; -import com.demcha.compose.font.FontName; -import com.demcha.testing.VisualTestOutputs; -import org.junit.jupiter.api.Test; - -import java.awt.Color; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Phase E.4 — renders the same {@link InvoiceDocumentSpec} once with the - * built-in {@link BusinessTheme#modern()} and once with a hand-built - * "Studio Emerald" {@code BusinessTheme}. The output lives under - * {@code target/visual-tests/custom-business-theme/} so a reviewer can - * open the two PDFs side-by-side and confirm the custom theme really - * does swap the visual identity (emerald primary + copper accent on - * ivory paper) without touching the template code. - * - *
The {@link #studioEmeraldTheme()} method mirrors the body of - * {@code examples/.../CustomBusinessThemeExample.studioEmeraldTheme()}; - * the example exists to be read, this test exists to keep that - * authoring path honest.
- * - * @author Artem Demchyshyn - */ -class CustomBusinessThemeDemoTest { - - @Test - void modernReferenceRendersToValidPdf() throws Exception { - renderWithTheme("invoice-modern-reference", BusinessTheme.modern()); - } - - @Test - void studioEmeraldRendersToValidPdf() throws Exception { - renderWithTheme("invoice-studio-emerald", studioEmeraldTheme()); - } - - @Test - void customThemeIsAcceptedByInvoiceTemplateV2() { - BusinessTheme custom = studioEmeraldTheme(); - // The hand-built theme must satisfy InvoiceTemplateV2's contract - // (i.e. all token slots populated) — constructing the template - // without an exception proves it. - InvoiceTemplateV2 template = new InvoiceTemplateV2(custom); - assertThat(template).isNotNull(); - assertThat(custom.name()).isEqualTo("studio-emerald"); - assertThat(custom.pageBackground()).isNotNull(); - } - - private static void renderWithTheme(String stem, BusinessTheme theme) throws Exception { - Path output = VisualTestOutputs.preparePdf(stem, "custom-business-theme"); - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .pageBackground(theme.pageBackground()) - .margin(DocumentInsets.of(28)) - .create()) { - new InvoiceTemplateV2(theme).compose(document, sampleInvoice()); - Files.write(output, document.toPdfBytes()); - } - byte[] bytes = Files.readAllBytes(output); - assertThat(bytes).isNotEmpty(); - assertThat(new String(bytes, 0, 5, StandardCharsets.US_ASCII)).isEqualTo("%PDF-"); - } - - private static BusinessTheme studioEmeraldTheme() { - DocumentPalette palette = DocumentPalette.of( - new Color(20, 80, 60), - new Color(176, 116, 56), - new Color(252, 248, 240), - new Color(238, 232, 218), - new Color(34, 38, 44), - new Color(110, 116, 124), - new Color(210, 200, 180)); - SpacingScale spacing = new SpacingScale(4.0, 7.0, 11.0, 18.0, 30.0); - TextScale text = new TextScale( - style(FontName.TIMES_BOLD, 28, DocumentTextDecoration.BOLD, palette.primary()), - style(FontName.TIMES_BOLD, 17, DocumentTextDecoration.BOLD, palette.primary()), - style(FontName.HELVETICA_BOLD, 12, DocumentTextDecoration.BOLD, palette.textPrimary()), - style(FontName.HELVETICA, 10, DocumentTextDecoration.DEFAULT, palette.textPrimary()), - style(FontName.HELVETICA, 9, DocumentTextDecoration.DEFAULT, palette.textMuted()), - style(FontName.HELVETICA_BOLD, 10, DocumentTextDecoration.BOLD, palette.textPrimary()), - style(FontName.HELVETICA_BOLD, 10, DocumentTextDecoration.BOLD, palette.accent())); - DocumentInsets cellPadding = DocumentInsets.symmetric(spacing.xs(), spacing.sm()); - DocumentStroke rule = DocumentStroke.of(palette.rule(), 0.5); - TablePreset table = new TablePreset( - DocumentTableStyle.builder() - .padding(cellPadding).fillColor(palette.surface()).stroke(rule).build(), - DocumentTableStyle.builder() - .padding(cellPadding).fillColor(palette.surfaceMuted()).stroke(rule).build(), - DocumentTableStyle.builder() - .padding(cellPadding).fillColor(palette.surfaceMuted()) - .stroke(DocumentStroke.of(palette.accent(), 0.8)).build(), - DocumentTableStyle.builder() - .padding(cellPadding).fillColor(palette.surfaceMuted()).stroke(rule).build()); - return new BusinessTheme("studio-emerald", palette, spacing, text, table, palette.surface()); - } - - private static DocumentTextStyle style(FontName font, - double size, - DocumentTextDecoration decoration, - DocumentColor color) { - return DocumentTextStyle.builder() - .fontName(font).size(size).decoration(decoration).color(color).build(); - } - - private static InvoiceDocumentSpec sampleInvoice() { - return InvoiceDocumentSpec.builder() - .title("Invoice") - .invoiceNumber("GC-2026-041") - .issueDate("02 Apr 2026") - .dueDate("16 Apr 2026") - .reference("Studio Emerald — Custom Theme") - .status("Pending") - .fromParty(party -> party - .name("GraphCompose Studio") - .addressLines("18 Layout Street", "London, UK", "EC1A 4GC") - .email("billing@graphcompose.dev") - .phone("+44 20 5555 1000")) - .billToParty(party -> party - .name("Northwind Systems") - .addressLines("Attn: Finance Team", "410 Market Avenue", "Manchester, UK") - .email("ap@northwind.example")) - .lineItem("Discovery workshop", "Stakeholder interviews", "1", "GBP 1,450", "GBP 1,450") - .lineItem("Template architecture", "Reusable flows", "2", "GBP 980", "GBP 1,960") - .lineItem("Render QA", "Visual validation passes", "3", "GBP 320", "GBP 960") - .summaryRow("Subtotal", "GBP 4,370") - .summaryRow("VAT (20%)", "GBP 874") - .totalRow("Total", "GBP 5,244") - .note("Brand-coloured invoice using Studio Emerald BusinessTheme.") - .paymentTerm("Payment due within 14 calendar days.") - .footerNote("Thank you for choosing GraphCompose for production document rendering.") - .build(); - } -} diff --git a/src/test/java/com/demcha/testing/visual/InvoiceTemplateV2DemoTest.java b/src/test/java/com/demcha/testing/visual/InvoiceTemplateV2DemoTest.java deleted file mode 100644 index be03f3971..000000000 --- a/src/test/java/com/demcha/testing/visual/InvoiceTemplateV2DemoTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.demcha.testing.visual; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.api.DocumentSession; -import com.demcha.compose.document.style.DocumentInsets; -import com.demcha.compose.document.templates.builtins.InvoiceTemplateV2; -import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; -import com.demcha.compose.document.theme.BusinessTheme; -import com.demcha.testing.VisualTestOutputs; -import org.junit.jupiter.api.Test; - -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Renders the same {@link InvoiceDocumentSpec} via {@code InvoiceTemplateV2} - * with each of the three built-in {@link BusinessTheme} themes. The output - * lives under {@code target/visual-tests/invoice-template-v2/} so a reviewer - * can flip through the three PDFs side-by-side and see the theme switch - * visually. - * - * @author Artem Demchyshyn - */ -class InvoiceTemplateV2DemoTest { - - @Test - void modernThemeRendersToValidPdf() throws Exception { - renderTheme("invoice-modern", BusinessTheme.modern()); - } - - @Test - void classicThemeRendersToValidPdf() throws Exception { - renderTheme("invoice-classic", BusinessTheme.classic()); - } - - @Test - void executiveThemeRendersToValidPdf() throws Exception { - renderTheme("invoice-executive", BusinessTheme.executive()); - } - - private static void renderTheme(String stem, BusinessTheme theme) throws Exception { - Path output = VisualTestOutputs.preparePdf(stem, "invoice-template-v2"); - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .pageBackground(theme.pageBackground()) - .margin(DocumentInsets.of(28)) - .create()) { - new InvoiceTemplateV2(theme).compose(document, sampleInvoice()); - Files.write(output, document.toPdfBytes()); - } - byte[] bytes = Files.readAllBytes(output); - assertThat(bytes).isNotEmpty(); - assertThat(new String(bytes, 0, 5, java.nio.charset.StandardCharsets.US_ASCII)) - .isEqualTo("%PDF-"); - } - - private static InvoiceDocumentSpec sampleInvoice() { - return InvoiceDocumentSpec.builder() - .title("Invoice") - .invoiceNumber("GC-2026-041") - .issueDate("02 Apr 2026") - .dueDate("16 Apr 2026") - .reference("Platform Refresh Sprint") - .status("Pending") - .fromParty(party -> party - .name("GraphCompose Studio") - .addressLines("18 Layout Street", "London, UK", "EC1A 4GC") - .email("billing@graphcompose.dev") - .phone("+44 20 5555 1000") - .taxId("GB-99887766")) - .billToParty(party -> party - .name("Northwind Systems") - .addressLines("Attn: Finance Team", "410 Market Avenue", "Manchester, UK") - .email("ap@northwind.example") - .phone("+44 161 555 2200")) - .lineItem("Discovery workshop", "Stakeholder interviews", "1", "GBP 1,450", "GBP 1,450") - .lineItem("Template architecture", "Reusable document flows", "2", "GBP 980", "GBP 1,960") - .lineItem("Render QA", "Visual validation passes", "3", "GBP 320", "GBP 960") - .lineItem("Developer enablement", "Examples module + onboarding notes", "1", "GBP 780", "GBP 780") - .summaryRow("Subtotal", "GBP 5,150") - .summaryRow("VAT (20%)", "GBP 1,030") - .totalRow("Total", "GBP 6,180") - .note("Please include the invoice number on your remittance advice.") - .note("All work was delivered as agreed during the April implementation window.") - .paymentTerm("Payment due within 14 calendar days.") - .paymentTerm("Bank transfer preferred.") - .footerNote("Thank you for choosing GraphCompose for production document rendering.") - .build(); - } -} diff --git a/src/test/java/com/demcha/testing/visual/ProposalTemplateV2DemoTest.java b/src/test/java/com/demcha/testing/visual/ProposalTemplateV2DemoTest.java deleted file mode 100644 index a66b237d6..000000000 --- a/src/test/java/com/demcha/testing/visual/ProposalTemplateV2DemoTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.demcha.testing.visual; - -import com.demcha.compose.GraphCompose; -import com.demcha.compose.document.api.DocumentSession; -import com.demcha.compose.document.style.DocumentInsets; -import com.demcha.compose.document.templates.builtins.ProposalTemplateV2; -import com.demcha.compose.document.templates.data.proposal.ProposalDocumentSpec; -import com.demcha.compose.document.theme.BusinessTheme; -import com.demcha.testing.VisualTestOutputs; -import org.junit.jupiter.api.Test; - -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Renders the same {@link ProposalDocumentSpec} via - * {@code ProposalTemplateV2} with each of the three built-in - * {@link BusinessTheme} themes. The output lives under - * {@code target/visual-tests/proposal-template-v2/} so a reviewer can - * flip through the three PDFs side-by-side and see the theme switch - * visually. - * - * @author Artem Demchyshyn - */ -class ProposalTemplateV2DemoTest { - - @Test - void modernThemeRendersToValidPdf() throws Exception { - renderTheme("proposal-modern", BusinessTheme.modern()); - } - - @Test - void classicThemeRendersToValidPdf() throws Exception { - renderTheme("proposal-classic", BusinessTheme.classic()); - } - - @Test - void executiveThemeRendersToValidPdf() throws Exception { - renderTheme("proposal-executive", BusinessTheme.executive()); - } - - private static void renderTheme(String stem, BusinessTheme theme) throws Exception { - Path output = VisualTestOutputs.preparePdf(stem, "proposal-template-v2"); - try (DocumentSession document = GraphCompose.document() - .pageSize(595, 842) - .pageBackground(theme.pageBackground()) - .margin(DocumentInsets.of(28)) - .create()) { - new ProposalTemplateV2(theme).compose(document, sampleProposal()); - Files.write(output, document.toPdfBytes()); - } - byte[] bytes = Files.readAllBytes(output); - assertThat(bytes).isNotEmpty(); - assertThat(new String(bytes, 0, 5, java.nio.charset.StandardCharsets.US_ASCII)) - .isEqualTo("%PDF-"); - } - - private static ProposalDocumentSpec sampleProposal() { - return ProposalDocumentSpec.builder() - .title("Proposal") - .proposalNumber("PROP-2026-014") - .preparedDate("02 Apr 2026") - .validUntil("16 Apr 2026") - .projectTitle("GraphCompose rollout for internal document operations") - .executiveSummary("This proposal describes a practical adoption path for reusable GraphCompose templates, render tests, and runnable examples across billing, hiring, and client-facing delivery workflows.") - .sender(party -> party - .name("GraphCompose Studio") - .addressLines("18 Layout Street", "London, UK", "EC1A 4GC") - .email("hello@graphcompose.dev") - .phone("+44 20 5555 1000") - .website("graphcompose.dev")) - .recipient(party -> party - .name("Northwind Systems") - .addressLines("Product Engineering", "410 Market Avenue", "Manchester, UK") - .email("platform@northwind.example") - .phone("+44 161 555 2200") - .website("northwind.example")) - .section("Scope", - "Introduce built-in invoice and proposal templates with a consistent business presentation layer.", - "Keep the production artifact clean by moving development-only preview code out of the published runtime scope.") - .section("Deliverables", - "Public DTOs and template interfaces for invoice and proposal rendering.", - "Render tests and a standalone examples module that generates PDF files on demand.") - .timelineItem("Week 1", "5 days", "Invoice API and first template delivery.") - .timelineItem("Week 2", "5 days", "Proposal layout, review loop, and render tests.") - .timelineItem("Week 3", "3 days", "Examples module and README handoff.") - .pricingRow("Foundation", "Template APIs and DTO modeling", "GBP 3,200") - .pricingRow("Document delivery", "Invoice and proposal templates with tests", "GBP 4,450") - .emphasizedPricingRow("Total investment", "Fixed-price project delivery", "GBP 9,500") - .acceptanceTerm("Proposal pricing is valid until the stated expiration date.") - .acceptanceTerm("Additional template families can be scoped in a separate phase.") - .footerNote("Prepared to demonstrate the business-document side of GraphCompose.") - .build(); - } -}