fix(documents): Document Center auto-fill completeness + form-set logic (Mark Willcott review)#207
Conversation
…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>
There was a problem hiding this comment.
💡 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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), |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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 👍 / 👎.
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
enrich(deal/API parity with the frontend).Seller_Name/FCD_Footer).Roof[0]→roof_zone,Thermal[0]→thermal_zone.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 shippedcompliance_borrowerspattern (not an overlay; the widget already exists). Degrades to buyer-only for single-buyer deals.serial_label_combined), matching TDHCA identity convention.Phase-2 entry-wizard (frontend)
DocumentCenter.jsx,constants.js,documentCenterMasks.test.js.)Phase-3 form-set logic
config/field_map.jsonform_set_rules+document_engine_v2._resolve_packet_templates).Verification
ruffclean.vite buildclean; 48 vitest passing.pypdfstays pinned at 6.12.2.Deliberately not changed (flags for review)
Internal_No_oral_promises.pdfleft 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.form_set_rulespending confirmation that THO uses it.Deploy
Merging to
maintriggers the existing CIbuild-and-deploy(Cloud Run, keyless WIF) followed byproduction_smoke.py+ healthz probes. No DNS / go-live changes in this PR.