Skip to content

fix(documents): Document Center auto-fill completeness + form-set logic (Mark Willcott review)#207

Merged
arigatoexpress merged 6 commits into
mainfrom
fix/tho-document-center-fields
Jun 19, 2026
Merged

fix(documents): Document Center auto-fill completeness + form-set logic (Mark Willcott review)#207
arigatoexpress merged 6 commits into
mainfrom
fix/tho-document-center-fields

Conversation

@arigatoexpress

Copy link
Copy Markdown
Owner

Document Center auto-fill completeness + form-set logic

Closes the auto-fill gaps from Mark Willcott's acceptance review so a fully-populated
deal stops producing blank fields on regulatory closing documents, plus the Phase-2
entry-wizard UX and Phase-3 packet form-set logic.

What's in this PR (6 commits)

Backend doc-fill correctness

  • Compose co-buyer name + mailing block in backend enrich (deal/API parity with the frontend).
  • Wire Statement-of-Ownership phones/installer + Property-Location home description; New/Used backstop.
  • PatriotAct (B3/B6): baked text overlays for buyer/co-buyer "Name Exactly as on Driver's License" and "Date of Birth" — these lines have no AcroForm widget (only Seller_Name/FCD_Footer).
  • Construction Standards Notice (A10): mapped the existing-but-unmapped Roof[0]roof_zone, Thermal[0]thermal_zone.
  • Plural "purchaser(s)" lines (B3/B4): new computed buyers_combined (" & ") mapped to the existing purchaser-name widget on the two arbitration agreements, the A/C acknowledgement, and the TMHA Limited Warranty PURCHASER line — mirrors the shipped compliance_borrowers pattern (not an overlay; the widget already exists). Degrades to buyer-only for single-buyer deals.
  • TMHA Limited Warranty I.D. NOS.: now carries serial number(s) and HUD label(s) per section (serial_label_combined), matching TDHCA identity convention.

Phase-2 entry-wizard (frontend)

  • Phone/SSN input masks, marital-status + length-of-employment dropdowns, factory/installer combo fields, co-buyer DOB, loan# → Portfolio backfill, 24 credit-app fields. (DocumentCenter.jsx, constants.js, documentCenterMasks.test.js.)

Phase-3 form-set logic

  • Config-driven packet de-duplication, used-home-only suppression on NEW packages, and an R020 flag gate (config/field_map.json form_set_rules + document_engine_v2._resolve_packet_templates).

Verification

  • Backend: 1114 passed / 11 skipped, ruff clean.
  • Frontend: vite build clean; 48 vitest passing.
  • Placement of every new fill visually verified on rendered PDFs (PatriotAct names/DOB, Construction Standards roof/thermal columns, Limited Warranty PURCHASER + I.D. NOS, arbitration purchaser line).
  • Regulatory templates untouched; pypdf stays pinned at 6.12.2.

Deliberately not changed (flags for review)

  • Internal_No_oral_promises.pdf left buyer-only: it's a singular "I, ___ … between myself and THO" attestation; combining two names reads as one signer. Convention (TDHCA forms + Tex. Occ. Code §1201) is one signed copy per purchaser.
  • Limited Warranty "Retailer License # (RBI)": no such field exists anywhere on the form — not fabricated. (RBI = THO's TDHCA license #35248, a retailer-block attribute.)
  • R020 deposit contract stays gated OFF by default in form_set_rules pending confirmation that THO uses it.

Deploy

Merging to main triggers the existing CI build-and-deploy (Cloud Run, keyless WIF) followed by production_smoke.py + healthz probes. No DNS / go-live changes in this PR.

arigatoexpress and others added 6 commits June 19, 2026 11:22
…rich

Document Center packages left fields blank that staff had filled in the entry
form (client report, Mark Willcott; "label number" flagged by Celeste).

Investigation finding: with a fully-populated customer every packet already
generates with all required fields rendering and no blank pages on the pinned
pypdf 6.12.2 — fill, the HUD/serial label numbers, and merge survival all work
(verified via scripts/validate_document_fills.py: 5/5 packets PASS). The
pypdf-breaks-AcroForm-fill lead does NOT apply to this version: a fill+readback
shows /V written and /AP baked.

The real backend gap: enrich_document_data composed the BUYER name and address
composites but not the CO-BUYER name or the MAILING block. The frontend
toDocumentData composes these, but the deal-based path
(Deal.to_document_data -> engine) and any other non-frontend caller relied on
the backstop, so co-buyer name and mailing address rendered blank on co-buyer
documents (Statement of Ownership, Consumer Disclosure, Credit App).

Fix: enrich now composes co_buyer_name from co_buyer_first/last and
mailing_city_state_zip / mailing_full_address from the mailing parts, mirroring
the existing buyer composition and the frontend. Pre-composed values are
preserved.

Tests: tests/test_document_fields_no_blanks.py asserts the composition and that
a generated Statement of Ownership / Sales Contract / Consumer Disclosure from a
fully-populated customer renders the previously-blank values (HUD label number,
serial, co-buyer name composed from parts only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sc; backend New/Used backstop

Phase 1 auto-fill mapping consistency (Mark Willcott acceptance review). The
same known value rendered on some templates but blank on others because the
PDF widget was simply unmapped in field_map.json. Wire the widgets that exist
but were unmapped, and add a backend backstop for the New/Used condition so
the deal/API path matches the frontend.

field_map.json (TDHCA_1023 Statement of Ownership):
- B2 Seller_Phone[0] <- seller_phone (retailer phone, THO default)
- B6 Phone1[0] <- buyer_phone; Page2 Creditor_Phone[0] <- creditor_phone
- B8 Installer_Name_Address[0]/Installer_Phone[0]/Installer_License[0]
  <- installer_* (THO defaults via enrichment unless installer_type=other)

field_map.json (Internal_PropertyLocation):
- B7 Description[0] <- manufacturer_model (the "Home Description:" line; the
  "Contract:" line is already wired via Portfolio[0])

document_quality.enrich_document_data:
- Single-source-of-truth New/Used resolution (_normalize_new_used): derive
  is_used/is_new/new_used_text from condition string or whichever flag is set,
  mirroring the frontend toDocumentData derivation. Both New and Used boxes no
  longer render blank on the deal-based path. No-signal input is left untouched
  so missing data is never masked.

Tests (test_document_fields_no_blanks.py): assert the SOO renders buyer,
creditor, and retailer phone; PropertyLocation renders the home description;
and the New/Used backstop derives correctly (incl. the no-signal no-op).

Full suite green (1085 pass / 11 skip), ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…4 credit-app fields, loan#/compliance

Implements the Mark Willcott review entry-wizard items (Phase 2) in
DocumentCenter.jsx, wired through field_map.json + enrich so the new fields
render on the generated PDFs.

Input masking (uncontrolled-input-safe via a `format` prop on Field that
rewrites the DOM value in place, preserving the focus-loss-free design):
- A1 phone mask 111-222-3333 on every phone (buyer, co-buyer, work, co-buyer
  work, creditor, installer-when-other, references).
- A3 SSN mask 111-22-3333 (buyer + co-buyer), with maxLength caps.

Constrained dropdowns:
- A4 Marital Status -> Married/Unmarried select (values match the backend
  _set_status_flags resolver that drives the credit-app checkbox pair).
- A5 Length-of-Employment -> numeric value + Years/Months unit, combined into
  the mapped occupation_length / co_buyer_occupation_length in toDocumentData.
- A6 Factory (manufacturer) + Installer-when-other -> config-driven ComboField
  (seed list in constants.js FACTORY_OPTIONS/INSTALLER_OPTIONS, an explicit
  "THO to supply full list" placeholder, and an "Other" free-text escape so an
  incomplete list never blocks staff; inventory auto-fill values still show).

New fields collected + wired:
- A7 Co-buyer DOB (buyer DOB already present).
- A2 Loan # on Financing; backfills the Portfolio[0] reference (Compliance
  Agreement + two-party contract) via toDocumentData and enrich.
- A8 Compliance Agreement entry section (Lender/Borrowers/Property Address);
  Compliance Agreement now maps Contract_Name -> compliance_borrowers and
  Portfolio[0] -> portfolio. Blank fields fall back to creditor / buyer names /
  installation address.
- The 24 omitted credit-app fields grouped into a "Credit Application Details"
  step (buyer/co-buyer employment + residence history, references, date_of_sale,
  seller_email, portfolio). These already had field_map mappings; the wizard
  simply now collects them and passes them through (toDocumentData spreads ...f).

Backend single-source-of-truth (enrich_document_data): portfolio<-loan_number
and compliance_borrowers<-buyer(+co_buyer) for the deal/API path; only backfills
when blank so explicit values and missing data are never masked.

field_map.json: added data_field_definitions for the newly-collected document
fields (buyer/co-buyer previous address+length, current payments, loan_number,
compliance_*). occupation_length_unit is a frontend-only helper (combined into
occupation_length) and is intentionally not a document field.

Verified: credit-app residence/employment/payment widgets, Deal Master Sheet
references, and the Compliance Agreement Portfolio + borrower line all fill
end-to-end with the spec record. Python 1090 pass/11 skip (+5), ruff clean,
frontend 48 vitest pass (+18), build OK, touched files eslint-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tandards roof/thermal zones

Mark Willcott acceptance review — fill auto-fill gaps that have no usable
AcroForm widget at the target location.

State_PatriotAct.pdf (B3/B6): the buyer/co-buyer 'Name Exactly as on Driver's
License' and 'Date of Birth' lines have no widget (only Seller_Name + FCD_Footer
exist), so they came out blank on every customer-identification form. Add baked
PDF_TEXT_OVERLAYS for buyer_name/buyer_dob/co_buyer_name/co_buyer_dob, carried
into pdf_data via synthetic THO_PatriotAct_* field_map keys. Coordinates
measured from the label baselines and visually verified on a filled render.

TMHA-ConstructionStandardsNotice.pdf (A10): the Roof[0] and Thermal[0] widgets
exist on the form but were unmapped, so the roof-load-zone and thermal-zone
lines were blank. Map Roof->roof_zone, Thermal->thermal_zone (real widgets, no
overlay). Visually verified the values land in the correct columns.

buyer_dob/co_buyer_dob are already defined pii=True, so logging sanitization is
unchanged. Extends tests/test_document_fields_no_blanks.py with FULL_CUSTOMER
identity dates + zones and two _assert_renders guards. Templates untouched;
pypdf stays 6.12.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on, R020 gate

Mark Willcott acceptance review (C1/C2/C3/C5). Config-driven form-set rules in
field_map.json applied at packet generation by resolve_form_set():

- C1: collapse exact-duplicate templates (Vacate-Home safety group + global pass)
- C2: dedupe Arbitration Agreement -> keep the Internal pre-filled copy, drop the
  TDHCA duplicate (same document the buyer signs once); revert note in config
- C3: suppress USED-home-only forms (TDHCA-ImproperSite, TDHCA_SitePreparation)
  on NEW packages, gated on resolved home condition; they remain on USED packets
- C5: gate the TMHA R020 Sales Contract & Deposit Agreement behind include_r020,
  DEFAULT-EXCLUDED from the standard NEW closing packet; template NOT deleted

full_closing_new (NEW) drops 45->41 docs (TDHCA arbitration dup, 2 used-home
forms, R020); merges cleanly with no failed templates. Static packet lists stay
the superset; new tests assert the NEW packet is free of duplicates, used-home
forms, and R020, and that USED packets keep used-home forms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… warranty I.D. NOS

Mark Willcott acceptance review (B3/B4). Research (TDHCA/TMHA forms + Tex. Occ.
Code 1201) showed these forms do NOT have a separate widget-less co-buyer line
as first assumed: each has a single purchaser-name AcroForm widget on a plural
'(purchaser(s) name(s))' / 'Buyer(s)' line. So the correct fix is mapping that
existing widget to a composed name, mirroring the shipped State_ComplianceAgreement
compliance_borrowers pattern -- NOT a baked overlay.

- enrich: add computed buyers_combined = '<buyer> & <co-buyer>' (degrades to the
  buyer alone for single-buyer deals), mirroring compliance_borrowers.
- field_map: repoint Contract_Name -> buyers_combined on TDHCA_ArbitrationAgreement,
  Internal_Arbitration_Agreement11, Internal_Air_Conditioning_Acknowledgement_Form,
  and the TMHA_LimitedWarranty PURCHASER line. Co-purchaser signature lines stay
  blank (wet ink).
- TMHA_LimitedWarranty I.D. NOS. line: repoint HUD -> serial_label_combined so it
  carries the serial number(s) AND HUD label(s) per section (TDHCA identity
  convention), instead of the HUD label alone.
- Register buyers_combined in data_field_definitions (computed).

Intentionally NOT changed, with rationale:
- Internal_No_oral_promises.pdf: singular 'I, ___ ... between myself and THO'
  attestation -- combining two names reads as one signer; left buyer-only (each
  co-purchaser signs their own copy). FLAG for Ari.
- TMHA_LimitedWarranty 'Retailer License# (RBI)': no such field exists anywhere on
  page 3 (verified via full page text); cannot fill without fabricating a field.
  FLAG for Ari.

Templates untouched; pypdf stays 6.12.2; backend-only (no DocumentCenter.jsx).
Adds 4 _assert_renders guards + a buyers_combined enrich unit test; full suite
1075 passed, ruff clean. Verified placement on filled renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 81cd7594fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Phase 3 form-set logic: dedupe same-document templates, suppress
# used-home-only forms on NEW packages, and drop flag-gated optionals
# (R020) before generating. The static packet list stays the superset.
templates = resolve_form_set(packet_config.get("templates", []), data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply form-set rules to batch packet selections

Applying resolve_form_set only in generate_packet() misses the Document Center’s normal packet path: selectPacket copies pk.templates from the static packet list, then handleGenerate posts those templates to /api/documents/generate-batch, where main.py calls engine_generate_batch without filtering. For full_closing_new, the static list still contains R020, the used-home-only forms, and duplicate arbitration, so selecting that packet in the UI still generates the forms this change is meant to suppress; apply the same resolution before listing/selecting packets or in generate_batch.

Useful? React with 👍 / 👎.

Comment on lines +636 to +640
creditor_name: f.creditor_name || f.compliance_lender,
compliance_borrowers: f.compliance_borrowers
|| joinNonEmpty([buyer, coBuyer], ' & '),
compliance_property_address: f.compliance_property_address
|| fullAddress(f.buyer_address, buyerCityStateZip),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use Compliance Agreement overrides before defaults

When staff fill Compliance Agreement-specific fields and the normal financing creditor/address also exist, these overrides are either ignored or not mapped: creditor_name prefers f.creditor_name over f.compliance_lender, and State_ComplianceAgreement still maps Install_Address to buyer_address in config/field_map.json, not compliance_property_address. The generated agreement will use the generic creditor/install address despite explicit inputs, which contradicts the new section’s “defaults when blank” behavior.

Useful? React with 👍 / 👎.

onChange={handleChange}
placeholder={placeholder}
readOnly={readOnly}
maxLength={maxLength}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let phone paste normalize before length limiting

When a field passes formatPhoneMask with maxLength={12}, browsers enforce maxLength before onChange, so pasting a common (281) 324-3020 value is truncated before the formatter can strip punctuation. The stored/rendered number becomes incomplete (for example 281-324-3), even though formatPhoneMask handles the full string in tests; rely on digit capping in the formatter or allow a longer raw input length.

Useful? React with 👍 / 👎.

@arigatoexpress arigatoexpress merged commit 9e317f2 into main Jun 19, 2026
2 checks passed
@arigatoexpress arigatoexpress deleted the fix/tho-document-center-fields branch June 19, 2026 20:28
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.

1 participant