Progressive Home — PointOfSale Knockout Surfacing
Card: POD9-408
Branch: feat/pod-9-extra-errors-catch
Status: Implemented — no QA sign-off required (see §Limitations)
Last updated: 2026-05-15
Context
Progressive Home is bound through the FAO (For Agents Only) portal. Our DTC integration duplicates the agent's session under our own Progressive credentials and drives the customer through the config-driven Fastlane Portal flow. Three steps at the end of the flow determine whether the risk is eligible to bind:
-
PointOfSale — Progressive evaluates the full risk profile (CLUE history, credit score, inspection status, prior claims). The HAL response for this step carries several boolean flags and a
PolicyMessageslist that signal ineligibility. In the FAO portal these surfaces map to modals and banners that the agent sees. In our DTC flow none of those UI elements exist — the gateway must read the raw HAL flags and route the customer to a Goosehead-branded knockout page. -
FinalSaleHO — E-sign consent and delivery email confirmation before binding.
-
FinalSalePropertySellQuote —
PUTcall that attempts to bind the policy. Progressive returns HTTP 200 even on bind failure; the error signal is inCurrentPageNavandEmbedded.SellQuotePropertyFailure.
This document is self-contained and can be used as the starting point for any future round of captures or implementation changes.
HAL Surface Map — PointOfSale
All paths below are relative to the root of the PointOfSale HAL response.
| Category | HAL Path | Trigger Value | Notes |
|---|---|---|---|
POS_APOLOGY_KICKOUT | Extenders.PropertyApologyKickout | "True" (PascalCase) | Hard underwriting apology. Also detected by detectSessionError in the HTTP client before any service layer sees the response — thrown as ProgressiveSessionKickoutError. |
POS_CREDIT_FAILURE | Embedded.PointOfSale.Extenders.ShouldShowKickoutCreditFailureModal | "Y" | Credit-score knockout modal. |
POS_CLUE_CLAIMS | Embedded.PointOfSale.PropertyClueClaims.Count > 0 AND ...POSPropertyBuyViewModel.Extenders.ShouldDisplayPropertyClueClaimsTable = "Y" | "Y" | CLUE loss-history table. Both conditions must be true; Count=0 alone does NOT fire (see R2 below). |
POS_INELIGIBLE_INSPECTION | ...POSPropertyBuyViewModel.Extenders.ShouldDisplayIneligibilityHomeInspMsg | "true" (lowercase) | Inspection requirement not satisfied. |
POS_INELIGIBLE | ...POSPropertyBuyViewModel.Extenders.ShouldDisplayIneligibilityMsg | "true" (lowercase) | General underwriting ineligibility. |
POS_WARNING_CODE | ...PolicyMessages.List[*] where MessageType="warning" and Code is in POS_KNOCKOUT_CODES | allowlist match | See §Confirmed KO Codes. |
Full path for ...POSPropertyBuyViewModel.Extenders.*:
Embedded.PointOfSale.ProductSpecificInformation.List[0].POSPropertyBuyViewModel.Extenders.<flag>
Full path for ...PolicyMessages.List[*]:
Embedded.PointOfSale.ProductSpecificInformation.List[0].PolicyMessages.List
Precedence (most specific wins):
POS_APOLOGY_KICKOUT → POS_CREDIT_FAILURE → POS_CLUE_CLAIMS → POS_INELIGIBLE_INSPECTION → POS_INELIGIBLE → POS_WARNING_CODE
Known non-KO warning codes (informational — do NOT add to allowlist)
| Code | Text |
|---|---|
NOHIT001 | Credit/CLUE no-hit — instructs agent to gather SSN |
NOCOV001 | No coverage in effect disclosure |
EFFDT001 | Effective date change may impact premium |
CLMFD001 | Claim Free Discount subject to change |
PRCHPRC001 | Purchase price requires underwriting review (price guidance, not a KO) |
Confirmed KO Codes — POS_KNOCKOUT_CODES
File: libs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-pos-knockout-codes.ts
| Code | Text | Source | Observable outcome |
|---|---|---|---|
PRIRCLM002 | "Claims require underwriting review prior to binding. Please gather details from each loss and contact underwriting." | R2 capture 2026-05-15: quote Q84545191, 937 Tara Dr, Burleson TX, 3 claims (2 weather + 1 non-weather) | FinalSale "Sale disabled" server-side — FinalSalePropertySellQuote never called |
How to add a new code:
- Confirm the code appears as
MessageType: "warning"in a real HAL capture (not speculative). - Add the string to
POS_KNOCKOUT_CODESwith a comment citing the source capture file. - Create a fixture in
__fixtures__/point-of-sale-<scenario>.jsonby copying the structure ofpoint-of-sale-prior-claims.jsonand substituting the code. - Add the fixture to
KNOCKOUT_CASESinprogressive-step-knockout.spec.ts. - Add a specific assertion in
progressive-point-of-sale-alert-detector.spec.ts.
FinalSale Error Surfaces
SellQuotePropertyFailure (HTTP 200 bind failure)
Progressive returns HTTP 200 but navigates to the failure page when the bind attempt fails at their end (system error, not underwriting KO).
Detection signal:
CurrentPageNav: "Workflow/.../FinalSaleGroup/FinalSaleHO/SellQuotePropertyFailure/SellQuotePropertyFailure"
Embedded.SellQuotePropertyFailure.Extenders.outlet: "modal"
Our handling: runFinalSaleSellQuote in progressive-step.service.ts detects this
and returns { advanced: false } instead of { advanced: true }. The portal keeps the
customer on the FinalSale step. The gateway logs a WARN with syncId for support.
FAO UI equivalent: Modal dialog titled "Sell Transaction Failed" with the message:
"An unexpected error has occurred that has prevented this quote from being sold. Please call the Agent Help Desk at 1-800-695-4050 with the quote number, <Q-NUMBER>, so we can help resolve this issue quickly."
This is a system error, not a PropertyApologyKickout (underwriting). Do not conflate them.
HAL Capture Log — All Rounds
How to capture
- Log in to
https://62.qa.foragentsonly.commanually (PingFederate, allow 60 s). - Start a new Home (HO3) quote for TX via the product picker.
- Navigate through: Named Insured → Products → Household → Additional Details → Coverages → Portfolio → PointOfSale.
- On PointOfSale: fill SSN, set
AtvOnPremise=No, click "Order Point of Sale" (#btnPosId). This firesPUT /Slot301/api/v1/composite/OrderPos— intercept this URL, notNextWorkflowState. - Advance to FinalSale (
#btnNextId), fillConfirmPrimaryEmailAddress, click "Sell Home" (#btnContinueId). - Save responses to
docs/progressive-home/.
Canonical test persona (single applicant):
| Field | Value |
|---|---|
| First name | Jhon |
| Last name | Doe |
| DOB | 01/01/1991 |
| SSN | 556-65-5665 |
| Phone | (888) 888-8888 |
| kepina6298@codoteam.com | |
| Agent code | 43786 / ASI: 440744 |
Address requirements: Must be a real USPS ZIP+4 address in TX. Fictional addresses fail on the Products step with "Submitted Address not Matched to Zip+4".
Capture results
Phase 8 grid (May 2026)
| # | Address | Quote | POS KO flags | FinalSale outcome | File |
|---|---|---|---|---|---|
| 1 | 937 Tara Dr, Burleson TX 76028 | Q84537703 | CLEAN | "Sell Transaction Failed" modal (QA system) | property-2-tara-burleson-POS-HAL.json |
| 2 | Monica Dr, Hardin TX (fictional) | — | BLOCKED | ZIP+4 mismatch | — |
| 3 | Marsh Wren Cir, Flint TX (fictional) | — | BLOCKED | ZIP+4 mismatch | — |
| 4 | Melville Cv, Austin TX (fictional) | — | BLOCKED | ZIP+4 mismatch | — |
| 5 | Bluffview Dr, Mesquite TX (fictional) | — | BLOCKED | ZIP+4 mismatch | — |
| 6 | 1230 Monica Dr, Hardin TX (Geller family) | Q84537482 | CLEAN | Apology/Index (system, expected QA) | married-1-geller-family_PointOfSale_OrderPos_response.json |
| 7 | Towne Square Dr, Plano TX (fictional) | — | BLOCKED | ZIP+4 mismatch | — |
| 8 | 7314 Birchtree Forest Dr, Houston TX 77088 | Q84537591 | CLEAN | Apology/Index (system, expected QA) | property-1-birchtree-houston_PointOfSale_OrderPos_response.json |
| 9 | Ovid Ave, Dallas TX (fictional) | — | BLOCKED | ZIP+4 mismatch | — |
Fast-follow runs (May 2026)
| Run | Scenario | Address | Quote | OrderPos flags | FinalSale | Files |
|---|---|---|---|---|---|---|
| R1 | Coastal | 1702 Avenue L, Galveston TX 77550 | — | CLEAN | Bind succeeded in QA (TST… policy) | run1-coastal-galveston-1702_OrderPos_200_after-mortgage_raw.json |
| R2 | Claims-heavy | 937 Tara Dr, Burleson TX 76028 | Q84545191 | PRIRCLM002 warning | "Sale disabled — Claims require underwriting review" | run2-claims-alex-claimsKo_Q84545191_OrderPos_200_raw.json |
| R3 | Old property (1952, crawl space) | 7314 Birchtree Forest Dr, Houston TX 77088 | Q84545599 | CLEAN | SellQuotePropertyFailure modal (QA system) | run3-old-birchtree-houston_Q84545599_OrderPos_200_raw.json, run3-old-birchtree-houston_Q84545599_FinalSalePropertySellQuote_200_raw.json |
R2 flag snapshot (claims-heavy, the key finding)
PropertyApologyKickout: null (no hard apology)
ShouldShowKickoutCreditFailureModal: "N"
PropertyClueClaims.Count: 0
ShouldDisplayPropertyClueClaimsTable: "Y" ← flag is Y but Count=0 → POS_CLUE_CLAIMS does NOT fire
ShouldDisplayIneligibilityMsg: "false"
ShouldDisplayIneligibilityHomeInspMsg:"false"
PolicyMessages:
PRIRCLM002 warning "Claims require underwriting review prior to binding..."
NOHIT001 warning "If the insurance score is a No Hit..." ← informational
null information "Both the claims history of the named insured..."
NOCOV001 information "No coverage is in effect at this time..."
EFFDT001 information "Changes in effective date may impact policy premium."
CLMFD001 information "Claim Free Discount is subject to change..."
PRIRCLM002 is the only warning code in the allowlist. It triggers POS_WARNING_CODE
knockout. The FAO FinalSale shows the sell button disabled server-side with label
"Sale disabled — Claims require underwriting review prior to binding."
FinalSalePropertySellQuote is never called.
R3 flag snapshot (old property, clean POS)
PropertyApologyKickout: null
ShouldShowKickoutCreditFailureModal: "N"
PropertyClueClaims.Count: 0
ShouldDisplayPropertyClueClaimsTable: "N"
ShouldDisplayIneligibilityMsg: "false"
ShouldDisplayIneligibilityHomeInspMsg:"false"
PolicyMessages:
NOHIT001 warning informational
null information ...
NOCOV001 information ...
EFFDT001 information ...
CLMFD001 information ...
FinalSale: CurrentPageNav → .../SellQuotePropertyFailure/SellQuotePropertyFailure
SellQuotePropertyFailure.Extenders.outlet: "modal"
KickoutHeader: null ← QA system error, NOT underwriting KO
Implementation — Code Locations
| What | File | Description |
|---|---|---|
| KO detector (6 surfaces) | libs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-point-of-sale-alert-detector.ts | ProgressivePointOfSaleAlertDetector.fromHalResponse() — pure function, no side effects |
| KO codes allowlist | libs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-pos-knockout-codes.ts | POS_KNOCKOUT_CODES set |
| Category precedence | libs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-knockout-reason-category.ts | KNOCKOUT_REASON_CATEGORY_PRECEDENCE |
| Goosehead copy (DTC-safe) | libs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-knockout-copy.ts | PROGRESSIVE_KNOCKOUT_COPY — never exposes Progressive verbatim copy |
| POS KO in step service | libs/apis/carriers/progressive/src/lib/application/services/progressive-step.service.ts | tryBuildPointOfSaleKnockoutStepConfig() — called on every PointOfSale response |
| SellQuotePropertyFailure | progressive-step.service.ts | isSellQuotePropertyFailure() + guard in runFinalSaleSellQuote() |
| HTTP client error detection | libs/apis/carriers/progressive/src/lib/infrastructure/http-api/progressive-http-api.client.ts | detectSessionError() — runs on every response regardless of HTTP status |
| DB persistence | apps/apis/fastlane-api-gateway/src/app/services/progressive-home-knockout-marker.service.ts | ProgressiveHomeKnockoutMarkerService.mark() — idempotent upsert |
| Controller error handlers | apps/apis/fastlane-api-gateway/src/app/controllers/progressive.controller.ts | getConfigDrivenErrorMessage(), persistKnockoutFromConfig(), checkKnockoutGate() |
| KO gate (short-circuit) | progressive.controller.ts | checkKnockoutGate() — fires before every endpoint touches Progressive |
| HAL test fixtures | libs/apis/carriers/progressive/src/lib/domain/value-objects/__fixtures__/ | One JSON per scenario — point-of-sale-*.json |
Error types and where they are caught
| Error class | Thrown by | Caught in controller | User-facing message |
|---|---|---|---|
ProgressiveSessionKickoutError | detectSessionError when PropertyApologyKickout="True" | submitStepConfig, stepConfig, go-to | Goosehead-branded KO page via buildSessionKickoutStepConfig |
ProgressiveSaveRaceConditionError | detectSessionError when SaveRaceCondition="True" | All config-driven endpoints | KO page via buildSaveRaceKnockoutStepConfig |
ProgressiveFieldValidationError | throwForFieldValidation on HTTP 400 with field edits | All config-driven endpoints | User-friendly message or transient if all edits are "Internal Server Error" |
ProgressiveQuoteUnavailableError | HTTP client when portal redirects to apology URL | getConfigDrivenErrorMessage | "Progressive could not open this quote. Please start a new quote from the agent portal." |
ProgressiveAuthSessionExpiredError | HTTP client when Progressive bounces to login | getConfigDrivenErrorMessage | "Your Progressive session expired. Please reopen the quote from the agent portal to continue." |
ExistingMortgageeIncompleteError | Mortgagee pre-check in step service | getConfigDrivenErrorMessage | Verbatim user-friendly message naming the offending slot and field |
Generic Error (session expired) | Session lookup fails with expired message | getConfigDrivenErrorMessage | "Your Progressive session expired. Please reopen the quote to continue." |
How to Run Another Capture Round
Use this checklist for any future round of HAL captures:
[ ] 1. Log in manually to https://62.qa.foragentsonly.com
Credentials: API_GATEWAY_PROGRESSIVE_USER_ID (NOT the numeric agent code)
PingFederate login — allow up to 60 s
[ ] 2. Start a fresh quote for TX / Home (HO3) via the product picker
Set #QuoteStateList = TX, click data-pp-id="HO", click "Start Quote"
[ ] 3. Use the canonical single-applicant persona (see §Canonical Test Persona above)
Never reuse a quote number from a previous run
[ ] 4. Walk through all steps manually (no "Populate Current Page" after Named Insured)
Products → Household → Additional Details → Coverages → Portfolio
[ ] 5. On PointOfSale: intercept **/OrderPos** BEFORE clicking #btnPosId
playwright_expect_response with url pattern: "**/OrderPos**"
Click #btnPosId first, then #btnNextId
[ ] 6. On FinalSale: fill ConfirmPrimaryEmailAddress = kepina6298@codoteam.com
Click #btnContinueId (Sell Home)
Capture FinalSalePropertySellQuote response if the button was enabled
[ ] 7. Save HAL files to docs/progressive-home/
Naming: <scenario-id>_<QuoteNumber>_OrderPos_200_raw.json
<scenario-id>_<QuoteNumber>_FinalSalePropertySellQuote_200_raw.json
[ ] 8. Extract flags using the Python snippet below and compare against §HAL Surface Map
[ ] 9. Classify findings:
CLEAN-ADVANCE → all KO flags are null / "N" / "false" / Count=0
KNOWN-CONFIRM → a flag we already handle fired as expected
LAND-NEW → a flag or code not yet in the implementation → add to §Confirmed KO Codes
Quick Python flag extractor (run from repo root):
import json, sys
def extract_flags(path, has_header=False):
with open(path) as f:
content = f.read()
if has_header:
content = content[content.index('\nBody: ') + len('\nBody: '):]
data = json.loads(content)
ext = data.get('Extenders', {})
pos = data.get('Embedded', {}).get('PointOfSale', {})
pos_ext = pos.get('Extenders', {})
psi0 = (pos.get('ProductSpecificInformation', {}).get('List') or [{}])[0]
vm_ext = psi0.get('POSPropertyBuyViewModel', {}).get('Extenders', {})
msgs = psi0.get('PolicyMessages', {}).get('List', [])
clue = pos.get('PropertyClueClaims', {})
return {
'PropertyApologyKickout': ext.get('PropertyApologyKickout'),
'ShouldShowKickoutCreditFailureModal': pos_ext.get('ShouldShowKickoutCreditFailureModal'),
'ClueClaims_Count': clue.get('Count'),
'ShouldDisplayPropertyClueClaimsTable': vm_ext.get('ShouldDisplayPropertyClueClaimsTable'),
'ShouldDisplayIneligibilityMsg': vm_ext.get('ShouldDisplayIneligibilityMsg'),
'ShouldDisplayIneligibilityHomeInspMsg':vm_ext.get('ShouldDisplayIneligibilityHomeInspMsg'),
'ActiveViewModelHasEdits': data.get('ActiveViewModelHasEdits'),
'PolicyMessages': [
{'code': m.get('Code'), 'type': m.get('MessageType'), 'text': m.get('Text','')[:80]}
for m in msgs
],
}
# Usage:
# python3 -c "$(cat above)" then call extract_flags('path/to/file.json', has_header=True/False)
Challenges Encountered
Angular form binding in FAO QA
FAO uses Angular reactive forms. Setting element.value = x does not trigger Angular's
internal model update. Required pattern for each field type:
// Standard input/select
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
setter?.call(el, value);
['input', 'change', 'blur'].forEach(e => el.dispatchEvent(new Event(e, { bubbles: true })));
// Radio buttons (must use MouseEvent, not just Event)
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
el.checked = true;
el.dispatchEvent(new Event('change', { bubbles: true }));
SSN and date masked inputs require manual entry — programmatic fill does not reliably bind due to the mask library's event model.
ActiveViewModelHasEdits: true blocking OrderPos
Any field left "dirty" in Angular's model causes the next HAL call to return
ActiveViewModelHasEdits: true, blocking advance. Symptom: 400 response from
NextWorkflowState. Fix: ensure all mandatory fields are properly bound before clicking
#btnPosId.
ShouldDisplayPropertyClueClaimsTable: "Y" with Count: 0
Observed in R2. Progressive sets the table-display flag to "Y" even when there are
no CLUE entries (Count: 0), apparently because prior claims exist in a different
system. The POS_CLUE_CLAIMS detector requires both Count > 0 AND the flag —
so it correctly does NOT fire in this scenario. The actual KO signal is PRIRCLM002
in PolicyMessages.
OrderPos endpoint vs NextWorkflowState
The "Order Point of Sale" button (#btnPosId) calls PUT /Slot301/api/v1/composite/OrderPos,
not NextWorkflowState. The PointOfSale flags only appear in the OrderPos response.
Intercepting NextWorkflowState misses the KO data entirely.
Limitations
No QA sign-off — card is shipped without traditional QA validation
The "Buy Online" button for Progressive Home does not enable with test data in DA.
Progressive requires the quote to reach specific underwriting states before enabling DTC binding, and our QA test data does not reach those states. This means:
- A QA engineer cannot navigate DA → Fastlane Portal → PointOfSale → KO page via the normal QA flow.
- KO scenarios (CLUE claims, credit failure, ineligibility, prior claims) are only reachable by running quotes directly in the FAO portal.
- The implementation is validated by:
- Unit tests against real/near-real HAL fixtures (see §Code Locations).
- Direct FAO QA captures confirming the HAL structure matches our detector's paths.
- Code review and manual inspection of the flag extraction logic.
KO flags never fired in QA environment
PropertyApologyKickout = "True", ShouldDisplayIneligibilityMsg = "true", and
ShouldShowKickoutCreditFailureModal = "Y" were never observed in any QA capture.
Progressive QA does not replicate the full underwriting ruleset. Fixtures for these
five categories are synthetic mutations of the clean baseline HAL — they match the
real structure but are not real captures. Only PRIRCLM002 (POS_WARNING_CODE) has
a real QA capture behind it.
POS_KNOCKOUT_CODES has one documented entry
Only PRIRCLM002 has been confirmed in QA. Other warning codes that Progressive may
emit in production (flood zone, fraud indicator, inspection failure codes) have not
been observed. The infrastructure is ready: adding a code is one line +
one fixture + two test additions.
FinalSaleHO knockouts use legacy detection
Knockouts that occur at AdditionalDetails and FinalSaleHO (outside PointOfSale)
are handled by ProgressiveKnockoutDetector (field-edit based, legacy). They do not
have the per-category copy, DB persistence, or POS_KNOCKOUT_CODES allowlist that this
implementation adds. Out of scope for POD9-408 v1.
Address pool is limited
Of the 9 Phase 8 addresses, only 3 are real USPS-verified TX addresses. The other 6 are fictional and block at the Products step. Future capture rounds should use only real addresses verified in advance via USPS ZIP+4 lookup.
Non-goals (explicitly out of scope)
- Surfacing the Risk Eligibility Letter PDF (
Extenders.PropertyApologyUrl) to the customer. - CMS-driven copy editing — all knockout copy is in TypeScript for v1.
- Auto (non-Home) KO surfacing — handled by a different flow.
- Progressive Home knockouts at
AdditionalDetails/FinalSaleHO— covered by legacy detector.