Sparse-State and Low-Coverage Refinements for Comparison UI
Issue: BUY-2530
Goal: BUY-2037 Goal
Status: Draft - Implementation Notes
Overview
This document defines the UI state handling for products with sparse data (missing prices, stale data) and low retailer coverage (1-2 retailers). These refinements ensure AI agents and users receive actionable signals when data quality is degraded.
1. Retailer Coverage States
1.1 States
| State | Retailer Count | UI Behavior |
|---|---|---|
| Full Coverage | 3+ retailers with prices | Standard comparison table |
| Low Coverage | 2 retailers | Condensed 2-column layout |
| Minimal Coverage | 1 retailer | Single-card with "Limited Data" warning |
| No Coverage | 0 retailers | Empty state with explanation |
1.2 Layout Specifications
Full Coverage (3+ retailers)
┌─────────────────────────────────────────────────────┐
│ Product Title [Cheapest] │
│ ───────────────────────────────────────────────── │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Retailer│ │ Retailer│ │ Retailer│ │ Retailer│ │
│ │ A │ │ B │ │ C │ │ D │ │
│ │ $Price │ │ $Price │ │ $Price │ │ $Price │ │
│ │ [Buy] │ │ [Buy] │ │ [Buy] │ │ [Buy] │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────┘
Low Coverage (2 retailers)
┌─────────────────────────────────────────────────────┐
│ Product Title ⚠ Limited Coverage │
│ ───────────────────────────────────────────────── │
│ ┌───────────────────┐ ┌───────────────────┐ │
│ │ Retailer A │ │ Retailer B │ │
│ │ $Price │ │ $Price │ │
│ │ Rating: 4.5★ │ │ Rating: 4.2★ │ │
│ │ [Buy Now] │ │ [Buy Now] │ │
│ └───────────────────┘ └───────────────────┘ │
│ │
│ 💡 More retailers coming soon │
└─────────────────────────────────────────────────────┘
Minimal Coverage (1 retailer)
┌─────────────────────────────────────────────────────┐
│ Product Title ⚠️ Limited Coverage │
│ ───────────────────────────────────────────────── │
│ ┌─────────────────────────────────────────┐ │
│ │ Retailer A Only │ │
│ │ │ │
│ │ $Price │ │
│ │ Rating: 4.5★ (234) │ │
│ │ │ │
│ │ [Buy Now] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ⚠️ Price comparisons unavailable - limited data │
└─────────────────────────────────────────────────────┘
2. Content Hierarchy: Warning vs Value Signal
2.1 Value Signals (Positive)
| Signal | Icon | Meaning |
|---|---|---|
| Best Price | 🏆 | Lowest price among available retailers |
| Best Rating | ⭐ | Highest rated option |
| Fastest Shipping | 🚚 | Lowest shipping days |
| Best Deal | 💰 | Best overall value (price + rating + availability) |
| In Stock | ✅ | Confirmed available |
| Fresh Data | 🟢 | Data < 24 hours old |
2.2 Warning Signals (Degraded Data)
| Signal | Icon | Meaning | Action Required |
|---|---|---|---|
| Stale Data | 🟡 | Data 7-30 days old | Verify price before purchase |
| Very Stale Data | 🔴 | Data > 30 days old | Do not recommend - may be outdated |
| Price Missing | ⚫ | No price available | Cannot compare |
| Partial Coverage | ⚠️ | < 3 retailers | Limited comparison |
| Retailer Unavailable | ⏸️ | Retailer API down | Retry later |
| Low Confidence | ❓ | Match score < 0.7 | Manual verification suggested |
3. Data Freshness Tiers
3.1 Tier Definitions
| Tier | Age Range | UI Treatment | is_available Behavior |
|---|---|---|---|
| Fresh | < 24 hours | Green badge | Trust value |
| Recent | 24h - 7 days | No badge | Trust value |
| Stale | 7 - 30 days | Yellow warning | Trust but show warning |
| Very Stale | > 30 days | Red warning | Set is_available: false |
3.2 Freshness Display Rules
function getFreshnessBadge(data_freshness: string): BadgeConfig {
switch (data_freshness) {
case "fresh":
return { color: "green", label: "Fresh", icon: "🟢" };
case "recent":
return { color: "none", label: "", icon: "" }; // No badge needed
case "stale":
return { color: "yellow", label: "Price may vary", icon: "🟡" };
case "very_stale":
return { color: "red", label: "Outdated", icon: "🔴" };
default:
return { color: "gray", label: "Unknown", icon: "❓" };
}
}
4. Price Missing States
4.1 Price Missing Reasons
| Reason | price_missing_reason | UI Message |
|---|---|---|
| Retailer API error | retailer_api_error | "Price unavailable - retailer API issue" |
| Retailer timeout | retailer_timeout | "Price check timed out - retry later" |
| Product unavailable | product_unavailable | "Product no longer available at this retailer" |
| Price not disclosed | price_not_disclosed | "Price not disclosed by retailer" |
| Crawl error | crawl_error | "Price data unavailable - scraping issue" |
4.2 Price Missing UI Handling
interface PriceMatch {
price: number | null;
price_missing: boolean;
price_missing_reason?: PriceMissingReason;
}
function renderPriceCell(match: PriceMatch): RenderedCell {
if (match.price_missing) {
return {
type: "warning",
message: getPriceMissingMessage(match.price_missing_reason),
icon: "⚫",
actionable: match.price_missing_reason === "retailer_timeout"
};
}
return {
type: "price",
value: formatPrice(match.price, match.currency)
};
}
5. API Signals Required for Each State
5.1 1-Retailer State (Minimal Coverage)
Minimum required signals:
{
"source_product_id": 12345,
"source_product_name": "Product Name",
"total_matches": 1,
"matches": [
{
"id": 12345,
"source": "shopee_sg",
"price": 99.00,
"currency": "SGD",
"is_available": true,
"data_freshness": "fresh",
"price_missing": false,
"price_missing_reason": null,
"coverage_note": "limited_coverage"
}
],
"highlights": {
"cheapest": { "id": 12345, "source": "shopee_sg", "price": 99.00 },
"best_rated": null,
"fastest_shipping": null
},
"meta": {
"retailer_count": 1,
"price_coverage_pct": 33.3,
"warning": "limited_coverage"
}
}
UI must show:
- Single card with warning banner: "⚠️ Limited Coverage - Only 1 retailer available"
- No comparison features (cannot compare with self)
- Suggest user search for alternatives
5.2 2-Retailer State (Low Coverage)
Minimum required signals:
{
"total_matches": 2,
"matches": [...],
"meta": {
"retailer_count": 2,
"price_coverage_pct": 66.7,
"warning": "low_coverage"
}
}
UI must show:
- Condensed 2-column layout
- "⚠️ Low Coverage" badge
- Cannot compute meaningful statistics (median, spread, etc.)
5.3 Stale Data State
Minimum required signals:
{
"matches": [
{
"id": 12345,
"source": "shopee_sg",
"price": 99.00,
"data_freshness": "stale",
"last_checked": "2026-04-09T10:00:00Z"
}
],
"meta": {
"stale_count": 1,
"fresh_count": 0
}
}
UI must show:
- Yellow warning badge on stale items
- "Last checked X days ago" timestamp
- Disclaimer: "Prices may have changed"
5.4 Very Stale Data State
Minimum required signals:
{
"matches": [
{
"id": 12345,
"source": "lazada_sg",
"price": 99.00,
"data_freshness": "very_stale",
"is_available": false,
"last_checked": "2026-04-01T08:00:00Z"
}
]
}
UI must show:
- Red warning badge
- "Outdated - Do not recommend" overlay
is_available: falsemakes product undismissable
6. Meta Fields for Coverage Tracking
6.1 Recommended Meta Fields
| Field | Type | Description |
|---|---|---|
retailer_count | int | Total retailers with this product |
price_coverage_pct | float | % of retailers with valid prices |
stale_count | int | Number of stale offers |
very_stale_count | int | Number of very stale offers |
warning | string | Top-level warning code |
unavailable_retailers | string[] | Retailers that failed to respond |
6.2 Meta Implementation
# In CompareResponse
meta: Optional[Dict[str, Any]] = Field(
None,
description="Metadata including price_coverage_pct, stale_count, very_stale_count"
)
def compute_meta(matches: List[CompareMatch]) -> Dict[str, Any]:
total = len(matches)
with_prices = sum(1 for m in matches if m.price is not None and not m.price_missing)
stale_count = sum(1 for m in matches if m.data_freshness == "stale")
very_stale_count = sum(1 for m in matches if m.data_freshness == "very_stale")
return {
"retailer_count": total,
"price_coverage_pct": round((with_prices / total * 100) if total > 0 else 0, 1),
"stale_count": stale_count,
"very_stale_count": very_stale_count,
"warning": "limited_coverage" if total < 3 else None
}
7. Implementation Checklist
- Fix BUG-1: Handle None prices in
_get_highlights(compare.py:65) - Populate
price_missingandprice_missing_reasonfields - Implement
data_freshnesstier classification - Populate
zero_priceflag for free items - Implement
unavailable_retailers[]tracking - Update frontend types with sparse-state handling
- Add coverage badges to UI components
- Add freshness indicators to price cards
- Document pagination behavior for sparse data
8. Backend Changes Required
8.1 Priority 1: Bug Fixes
- Fix TypeError in
_get_highlightswhen prices are None
8.2 Priority 2: Data Population
- Implement
data_freshnesscalculation based onupdated_atorlast_checked - Populate
price_missing_reasonwhen scraping fails - Track
unavailable_retailerswhen retailer APIs fail
8.3 Priority 3: New Meta Fields
- Add
compute_meta()helper to build coverage metadata - Return
metaobject in all compare responses - Document meta field semantics
Document version: 1.0
Created for BUY-2530 sparse-state and low-coverage refinements