Skip to main content

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:

  1. 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 PolicyMessages list 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.

  2. FinalSaleHO — E-sign consent and delivery email confirmation before binding.

  3. FinalSalePropertySellQuotePUT call that attempts to bind the policy. Progressive returns HTTP 200 even on bind failure; the error signal is in CurrentPageNav and Embedded.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.

CategoryHAL PathTrigger ValueNotes
POS_APOLOGY_KICKOUTExtenders.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_FAILUREEmbedded.PointOfSale.Extenders.ShouldShowKickoutCreditFailureModal"Y"Credit-score knockout modal.
POS_CLUE_CLAIMSEmbedded.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_CODESallowlist matchSee §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_KICKOUTPOS_CREDIT_FAILUREPOS_CLUE_CLAIMSPOS_INELIGIBLE_INSPECTIONPOS_INELIGIBLEPOS_WARNING_CODE

Known non-KO warning codes (informational — do NOT add to allowlist)

CodeText
NOHIT001Credit/CLUE no-hit — instructs agent to gather SSN
NOCOV001No coverage in effect disclosure
EFFDT001Effective date change may impact premium
CLMFD001Claim Free Discount subject to change
PRCHPRC001Purchase 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

CodeTextSourceObservable 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:

  1. Confirm the code appears as MessageType: "warning" in a real HAL capture (not speculative).
  2. Add the string to POS_KNOCKOUT_CODES with a comment citing the source capture file.
  3. Create a fixture in __fixtures__/point-of-sale-<scenario>.json by copying the structure of point-of-sale-prior-claims.json and substituting the code.
  4. Add the fixture to KNOCKOUT_CASES in progressive-step-knockout.spec.ts.
  5. 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

  1. Log in to https://62.qa.foragentsonly.com manually (PingFederate, allow 60 s).
  2. Start a new Home (HO3) quote for TX via the product picker.
  3. Navigate through: Named Insured → Products → Household → Additional Details → Coverages → Portfolio → PointOfSale.
  4. On PointOfSale: fill SSN, set AtvOnPremise=No, click "Order Point of Sale" (#btnPosId). This fires PUT /Slot301/api/v1/composite/OrderPos — intercept this URL, not NextWorkflowState.
  5. Advance to FinalSale (#btnNextId), fill ConfirmPrimaryEmailAddress, click "Sell Home" (#btnContinueId).
  6. Save responses to docs/progressive-home/.

Canonical test persona (single applicant):

FieldValue
First nameJhon
Last nameDoe
DOB01/01/1991
SSN556-65-5665
Phone(888) 888-8888
Emailkepina6298@codoteam.com
Agent code43786 / 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)

#AddressQuotePOS KO flagsFinalSale outcomeFile
1937 Tara Dr, Burleson TX 76028Q84537703CLEAN"Sell Transaction Failed" modal (QA system)property-2-tara-burleson-POS-HAL.json
2Monica Dr, Hardin TX (fictional)BLOCKEDZIP+4 mismatch
3Marsh Wren Cir, Flint TX (fictional)BLOCKEDZIP+4 mismatch
4Melville Cv, Austin TX (fictional)BLOCKEDZIP+4 mismatch
5Bluffview Dr, Mesquite TX (fictional)BLOCKEDZIP+4 mismatch
61230 Monica Dr, Hardin TX (Geller family)Q84537482CLEANApology/Index (system, expected QA)married-1-geller-family_PointOfSale_OrderPos_response.json
7Towne Square Dr, Plano TX (fictional)BLOCKEDZIP+4 mismatch
87314 Birchtree Forest Dr, Houston TX 77088Q84537591CLEANApology/Index (system, expected QA)property-1-birchtree-houston_PointOfSale_OrderPos_response.json
9Ovid Ave, Dallas TX (fictional)BLOCKEDZIP+4 mismatch

Fast-follow runs (May 2026)

RunScenarioAddressQuoteOrderPos flagsFinalSaleFiles
R1Coastal1702 Avenue L, Galveston TX 77550CLEANBind succeeded in QA (TST… policy)run1-coastal-galveston-1702_OrderPos_200_after-mortgage_raw.json
R2Claims-heavy937 Tara Dr, Burleson TX 76028Q84545191PRIRCLM002 warning"Sale disabled — Claims require underwriting review"run2-claims-alex-claimsKo_Q84545191_OrderPos_200_raw.json
R3Old property (1952, crawl space)7314 Birchtree Forest Dr, Houston TX 77088Q84545599CLEANSellQuotePropertyFailure 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

WhatFileDescription
KO detector (6 surfaces)libs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-point-of-sale-alert-detector.tsProgressivePointOfSaleAlertDetector.fromHalResponse() — pure function, no side effects
KO codes allowlistlibs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-pos-knockout-codes.tsPOS_KNOCKOUT_CODES set
Category precedencelibs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-knockout-reason-category.tsKNOCKOUT_REASON_CATEGORY_PRECEDENCE
Goosehead copy (DTC-safe)libs/apis/carriers/progressive/src/lib/domain/value-objects/progressive-knockout-copy.tsPROGRESSIVE_KNOCKOUT_COPY — never exposes Progressive verbatim copy
POS KO in step servicelibs/apis/carriers/progressive/src/lib/application/services/progressive-step.service.tstryBuildPointOfSaleKnockoutStepConfig() — called on every PointOfSale response
SellQuotePropertyFailureprogressive-step.service.tsisSellQuotePropertyFailure() + guard in runFinalSaleSellQuote()
HTTP client error detectionlibs/apis/carriers/progressive/src/lib/infrastructure/http-api/progressive-http-api.client.tsdetectSessionError() — runs on every response regardless of HTTP status
DB persistenceapps/apis/fastlane-api-gateway/src/app/services/progressive-home-knockout-marker.service.tsProgressiveHomeKnockoutMarkerService.mark() — idempotent upsert
Controller error handlersapps/apis/fastlane-api-gateway/src/app/controllers/progressive.controller.tsgetConfigDrivenErrorMessage(), persistKnockoutFromConfig(), checkKnockoutGate()
KO gate (short-circuit)progressive.controller.tscheckKnockoutGate() — fires before every endpoint touches Progressive
HAL test fixtureslibs/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 classThrown byCaught in controllerUser-facing message
ProgressiveSessionKickoutErrordetectSessionError when PropertyApologyKickout="True"submitStepConfig, stepConfig, go-toGoosehead-branded KO page via buildSessionKickoutStepConfig
ProgressiveSaveRaceConditionErrordetectSessionError when SaveRaceCondition="True"All config-driven endpointsKO page via buildSaveRaceKnockoutStepConfig
ProgressiveFieldValidationErrorthrowForFieldValidation on HTTP 400 with field editsAll config-driven endpointsUser-friendly message or transient if all edits are "Internal Server Error"
ProgressiveQuoteUnavailableErrorHTTP client when portal redirects to apology URLgetConfigDrivenErrorMessage"Progressive could not open this quote. Please start a new quote from the agent portal."
ProgressiveAuthSessionExpiredErrorHTTP client when Progressive bounces to logingetConfigDrivenErrorMessage"Your Progressive session expired. Please reopen the quote from the agent portal to continue."
ExistingMortgageeIncompleteErrorMortgagee pre-check in step servicegetConfigDrivenErrorMessageVerbatim user-friendly message naming the offending slot and field
Generic Error (session expired)Session lookup fails with expired messagegetConfigDrivenErrorMessage"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:
    1. Unit tests against real/near-real HAL fixtures (see §Code Locations).
    2. Direct FAO QA captures confirming the HAL structure matches our detector's paths.
    3. 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.