Spaces:
Sleeping
fix(#57+#58+#59): canonical-resolve compare-card marketplace lookup
Browse filesOne root cause, three symptoms on the HDFC ERGO Optima Restore compare
card. page.tsx policyById was keyed ONLY by exact marketplace policy_id;
the compare modal resolves the marketplace row by the CITED policy_id. A
recommendation can cite a doctype/variant/alias id that is not byte-equal
to the marketplace card's id (same canonical-identity class #40 solved
for grades) β policyById[id] undefined β the card silently degraded:
- #58 no network_list_url β 'Hospitals list' link missing
- #57 no rationalised SI β Cover amount fell back to 'As per policy schedule'
- #59 fewer populated fields β shorter/asymmetric vs Niva Bupa & Star
Fix: build a canonical-resolving index (exact policy_id wins, plus
doctype-stripped product_key + normalised policy_name + aliases, never
overwriting a real id hit) and a _resolvePolicy(id,name) fallback chain;
PolicyCompareModal now passes c.policy_name as the canonical fallback.
Niva Bupa & Star already matched exactly so they were unaffected; HDFC
now resolves to its real marketplace card (link + SI + full fields).
tsc --noEmit clean; frontend build Compiled successfully.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@@ -3877,14 +3877,44 @@ function CitedPolicyCards({
|
|
| 3877 |
// same 4-stat grid + highlights that the marketplace cards show. When the
|
| 3878 |
// marketplace hasn't loaded yet (or a cited policy isn't in the corpus),
|
| 3879 |
// the modal's PolicyHighlights section silently skips.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3880 |
const policyById: Record<string, MarketplacePolicy> = (() => {
|
| 3881 |
const out: Record<string, MarketplacePolicy> = {};
|
| 3882 |
if (!marketplace) return out;
|
|
|
|
|
|
|
|
|
|
| 3883 |
for (const p of marketplace.policies) {
|
| 3884 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3885 |
}
|
| 3886 |
return out;
|
| 3887 |
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3888 |
|
| 3889 |
// Translate the live UserProfile (chat-side) into the shapes the
|
| 3890 |
// premium-bulk and scorecard-bulk endpoints expect. These two shapes are
|
|
@@ -4040,7 +4070,7 @@ function CitedPolicyCards({
|
|
| 4040 |
policies={topPolicies}
|
| 4041 |
onClose={() => setCompareOpen(false)}
|
| 4042 |
profile={profile}
|
| 4043 |
-
policyDataFor={(id) =>
|
| 4044 |
renderPremiumFor={(policyId, policyName) => (
|
| 4045 |
<PolicyPremiumWidget
|
| 4046 |
policyId={policyId}
|
|
|
|
| 3877 |
// same 4-stat grid + highlights that the marketplace cards show. When the
|
| 3878 |
// marketplace hasn't loaded yet (or a cited policy isn't in the corpus),
|
| 3879 |
// the modal's PolicyHighlights section silently skips.
|
| 3880 |
+
// Canonical-resolving index (#57/#58/#59 root cause): the compare modal
|
| 3881 |
+
// looks up the marketplace row by the CITED policy_id. A recommendation
|
| 3882 |
+
// can cite a doctype/variant/alias id that is not byte-equal to the
|
| 3883 |
+
// marketplace card's policy_id (same canonical-identity class #40 solved
|
| 3884 |
+
// for grades). An exact-only map then returns undefined β the card loses
|
| 3885 |
+
// its Hospitals link, SI falls back to "As per policy schedule", and it
|
| 3886 |
+
// renders fewer fields (asymmetric). So we also register weaker canonical
|
| 3887 |
+
// keys: the doctype-stripped product_key, the normalised policy_name, and
|
| 3888 |
+
// every alias name β without ever overwriting a real exact-id hit.
|
| 3889 |
+
const _DOCT_RE = /__(wordings|brochure|cis|prospectus)$/;
|
| 3890 |
+
const _pkOf = (s: string) => (s || "").replace(_DOCT_RE, "");
|
| 3891 |
+
const _nmKey = (s: string) =>
|
| 3892 |
+
"nm:" + (s || "").trim().toLowerCase().replace(/\s+/g, " ");
|
| 3893 |
const policyById: Record<string, MarketplacePolicy> = (() => {
|
| 3894 |
const out: Record<string, MarketplacePolicy> = {};
|
| 3895 |
if (!marketplace) return out;
|
| 3896 |
+
// Pass 1 β exact policy_id is the strongest key; set first.
|
| 3897 |
+
for (const p of marketplace.policies) out[p.policy_id] = p;
|
| 3898 |
+
// Pass 2 β weaker canonical keys, only when not already a real id hit.
|
| 3899 |
for (const p of marketplace.policies) {
|
| 3900 |
+
const k = _pkOf(p.policy_id);
|
| 3901 |
+
if (k && !(k in out)) out[k] = p;
|
| 3902 |
+
const nk = _nmKey(p.policy_name);
|
| 3903 |
+
if (nk !== "nm:" && !(nk in out)) out[nk] = p;
|
| 3904 |
+
for (const a of p.aliases ?? []) {
|
| 3905 |
+
const ak = _nmKey(a);
|
| 3906 |
+
if (ak !== "nm:" && !(ak in out)) out[ak] = p;
|
| 3907 |
+
}
|
| 3908 |
}
|
| 3909 |
return out;
|
| 3910 |
})();
|
| 3911 |
+
const _resolvePolicy = (
|
| 3912 |
+
id: string,
|
| 3913 |
+
name?: string,
|
| 3914 |
+
): MarketplacePolicy | undefined =>
|
| 3915 |
+
policyById[id] ??
|
| 3916 |
+
policyById[_pkOf(id)] ??
|
| 3917 |
+
(name ? policyById[_nmKey(name)] : undefined);
|
| 3918 |
|
| 3919 |
// Translate the live UserProfile (chat-side) into the shapes the
|
| 3920 |
// premium-bulk and scorecard-bulk endpoints expect. These two shapes are
|
|
|
|
| 4070 |
policies={topPolicies}
|
| 4071 |
onClose={() => setCompareOpen(false)}
|
| 4072 |
profile={profile}
|
| 4073 |
+
policyDataFor={(id, name) => _resolvePolicy(id, name)}
|
| 4074 |
renderPremiumFor={(policyId, policyName) => (
|
| 4075 |
<PolicyPremiumWidget
|
| 4076 |
policyId={policyId}
|
|
@@ -241,7 +241,15 @@ export type PolicyCompareModalProps = {
|
|
| 241 |
// Optional resolver returning the marketplace MarketplacePolicy row for a
|
| 242 |
// citation. Powers the new "POLICY HIGHLIGHTS" section (4-stat grid +
|
| 243 |
// bullets). When undefined, the highlights section is skipped.
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
// Hook for "Open in full marketplace" β defaults to no-op + closes modal.
|
| 246 |
onOpenMarketplace?: () => void;
|
| 247 |
};
|
|
@@ -428,7 +436,7 @@ export default function PolicyCompareModal({
|
|
| 428 |
c.policy_id,
|
| 429 |
c.policy_name,
|
| 430 |
)}
|
| 431 |
-
marketplacePolicy={policyDataFor?.(c.policy_id)}
|
| 432 |
profile={_profile as SnapProfile}
|
| 433 |
/>
|
| 434 |
</div>
|
|
|
|
| 241 |
// Optional resolver returning the marketplace MarketplacePolicy row for a
|
| 242 |
// citation. Powers the new "POLICY HIGHLIGHTS" section (4-stat grid +
|
| 243 |
// bullets). When undefined, the highlights section is skipped.
|
| 244 |
+
// Resolve by cited policy_id; policyName is a canonical fallback so a
|
| 245 |
+
// doctype/variant/alias id (e.g. recommended `hdfc-ergo__optima-restore`
|
| 246 |
+
// vs marketplace card `..__brochure`) still resolves to its card β
|
| 247 |
+
// otherwise the card silently degrades (no Hospitals link, SI falls back
|
| 248 |
+
// to "As per policy schedule", fewer fields β asymmetric). #57/#58/#59.
|
| 249 |
+
policyDataFor?: (
|
| 250 |
+
policyId: string,
|
| 251 |
+
policyName?: string,
|
| 252 |
+
) => MarketplacePolicy | undefined;
|
| 253 |
// Hook for "Open in full marketplace" β defaults to no-op + closes modal.
|
| 254 |
onOpenMarketplace?: () => void;
|
| 255 |
};
|
|
|
|
| 436 |
c.policy_id,
|
| 437 |
c.policy_name,
|
| 438 |
)}
|
| 439 |
+
marketplacePolicy={policyDataFor?.(c.policy_id, c.policy_name)}
|
| 440 |
profile={_profile as SnapProfile}
|
| 441 |
/>
|
| 442 |
</div>
|