Skip to content

feat(templates): add the structured proposal document model - #609

Merged
DemchaAV merged 1 commit into
feature/template-promotionfrom
feat/proposal-structured-data
Aug 30, 2026
Merged

DemchaAV merged 1 commit into
feature/template-promotionfrom
feat/proposal-structured-data

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

The proposal family's data layer carries one shape — ProposalData's titled prose sections with a flat timeline and pricing list. The structured business proposal look (brand marks, an authored multi-line title, an at-a-glance fact card, goal cells, a numbered scope list, authored deliverable columns, a phase grid with its own headers, priced rows with roles, a signing card) cannot be represented in it — and both ProposalData and ProposalDocumentSpec are records, so the existing model cannot grow those structures compatibly.

What changed

  • templates.data.proposal gains a second, structured document model beside the narrative one: StructuredProposalData (fluent builder, 12 components) wrapped by the thin StructuredProposalDocumentSpec carrier (from()), plus the section records — ProposalBrand, ProposalTitleLines (lead/second/third), ProposalMetaLine, ProposalSummaryBlock, ProposalGlance (+Fact with icon/label/value/optional note), ProposalGoals (+Goal), ProposalScope (+numbered Item), ProposalDeliverables (two authored columns), ProposalPhaseGrid (+Phase, headers as an authored list — the column set belongs to the preset that draws the grid, unlike ProposalInvestment, whose two columns are fixed by the concept and therefore named fields), ProposalInvestment (+Row with a Role: NONE / SUBTOTAL / OPTIONAL — named for what the row is, not how it looks — and a total band), ProposalTermsBlock, ProposalAcceptance (statement + signature field labels).
  • The near-duplications with the narrative records are deliberate and each is justified in Javadoc: ProposalScope.Item vs ProposalSection (numbered title+description row vs titled prose run), ProposalPhaseGrid.Phase vs ProposalTimelineItem (number/focus/output columns + grid-owned headers vs phase/duration/details triple), ProposalInvestment.Row vs ProposalPricingRow (row roles + separate total band vs flat list).
  • Family conventions hold: compact-constructor normalization (null"" / List.of(), Role.NONE), List.copyOf freeze on all 11 collections, no rendering/DSL/Jackson dependencies, every public member documented, largest file 247 LOC. The section records construct positionally; the top-level builder is where a document is assembled. package-info now names both models and which presets consume which. The narrative model is untouched.
  • Consumers arrive separately: the first structured preset (the ported northline-proposal look) follows in its own PR, with a second variant of the same look behind it.

Verification

./mvnw -B -ntp verify -pl :graph-compose-templatesBUILD SUCCESS, 111 tests green, +7 new (StructuredProposalDataTest: empty-form normalization for all 12 components, null-string sweep across the leaf records and every section's heading/icon, Role default, frozen-collection sweep across all 11 lists, full builder pass-through, spec normalization + from()); javadoc:javadoc -pl :graph-compose-templates → 0 warnings. Additive public API only — no existing type changed.

Lane: canonical (templates.data.proposal) — additive data-layer API; no engine changes.

The proposal family's data layer knew one shape - a titled run of prose
sections with a flat timeline and pricing list - which cannot carry the
structured business proposal: brand marks, an authored multi-line title,
an at-a-glance fact card, goal cells, a numbered scope list, authored
deliverable columns, a phase grid with its own headers, priced rows with
a Role (NONE / SUBTOTAL / OPTIONAL) and a total band, and a signing
card. templates.data.proposal now carries that second model -
StructuredProposalData (+ its section records) wrapped by
StructuredProposalDocumentSpec - alongside the narrative one; a preset
consumes the model whose shape it renders. Every component normalizes
null to its empty form and freezes its collections, matching the
family's existing records.
/**
* Normalizes optional fields and freezes both columns.
*/
public ProposalDeliverables {
@DemchaAV
DemchaAV changed the base branch from develop to feature/template-promotion August 30, 2026 17:57
@DemchaAV
DemchaAV merged commit 2abfa20 into feature/template-promotion Aug 30, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the feat/proposal-structured-data branch August 30, 2026 19:05
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