Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Design: Fix Missing AD Restaurants on Group Landing Page

Date: 2026-06-29
Status: Approved
Scope: Option A — surgical fix (G1 + G2 + G3)


Problem

AD (sponsored/advertisement) restaurants are returned by the GroupLandingRestaurants GraphQL API but silently dropped before they reach the UI. The bug affects any group landing page that has sponsored restaurants in its list — observed at /en/famous-and-special-restaurants?city_id=1.

Root Cause

The backend deduplication contract: when multiple entries share the same branchId, the compact-restaurant REST API returns isMerged: false on entries that must not be merged — they should appear as independent cards (e.g. an organic card AND a sponsored card for the same branch). hh-pegasus respects this flag; the monorepo port missed it entirely.

Call chain where the bug lives:

useGroupLandingRestaurants
  → getRestaurantList (shared-service)
    → getGroupLandingRestaurant (shared-api, GraphQL) ← returns both entries
    → mergeSearchResult (shared-api/common)           ← BUG: drops the AD entry here
      → getCompactRestaurants (shared-api)            ← compact has isMerged: false

Inside mergeSearchResult, when a restaurant has a compact version:

  • hh-pegasus: checks compact.attributes.isMerged === false → pushes the original entry unchanged and skips merging
  • Monorepo: no check → falls into normal merge path → second occurrence (existingIndex !== undefined) hits continue → the AD entry is silently dropped

The isMerged field is also completely absent from the monorepo’s shared-type package, so even if the guard were written, compact.attributes.isMerged would be undefined at runtime (TypeScript casts it away). Both the type and the guard must be fixed together.


Solution

G1 + G2: Restore the isMerged guard (shared-type + shared-api)

File 1: packages/shared-type/src/Restaurant.ts

Add isMerged to the compact/featured restaurant attributes Valibot schema. This is the field the backend compact-restaurant API sets to signal merge intent.

// In the FeaturedRestaurantAttributes object schema, alongside isAds:
isMerged: optional(boolean()),

The inferred TypeScript type picks this up automatically — no manual interface edit needed.

File 2: packages/shared-api/src/common/mergeSearchResult.ts

Restore the guard block immediately after if (compact) {:

if (compact) {
  // Restore from hh-pegasus: backend marks isMerged: false to prevent
  // deduplication (e.g. organic + sponsored entries for the same branch).
  if (compact.attributes.isMerged === false) {
    mergedRestaurants.push(restaurant);
    continue;
  }
  // ... existing merge logic unchanged
}

G3: Restore rankingId propagation (same file)

On first-seen merge, copy the original entry’s ranking ID to the compact version so impression tracking stays intact:

// After: compact.attributes.branchRestaurantIds = [String(restaurant.id)]
compact.rankingId = restaurant.rankingId; // was requestId in pegasus; renamed in monorepo

Testing

File: packages/shared-api/src/common/__tests__/mergeSearchResult.test.ts

Add one new test case:

“keeps both entries when compact has isMerged: false”
Arrange: two restaurants with the same branchId; first is organic, second has isAds: true; compact API returns isMerged: false.
Assert: result array has 2 entries — both the organic and the AD entry — in their original form (not replaced by compact).

No changes to existing test cases.


Files Changed

FileChange
packages/shared-type/src/Restaurant.tsAdd isMerged: optional(boolean()) to FeaturedRestaurantAttributes Valibot schema
packages/shared-api/src/common/mergeSearchResult.tsAdd isMerged === false guard + compact.rankingId = restaurant.rankingId
packages/shared-api/src/common/__tests__/mergeSearchResult.test.tsAdd 1 test case for isMerged: false

Out of Scope

  • G4 (sortBy store-fallback drift in getRestaurantList) — not causing visible bugs; fix separately if needed.
  • G5–G8 — confirmed no-ops or already handled.
  • UI changes — AD label rendering is already parity-complete in both pegasus and monorepo.
  • Dynamic group landing worker setup — not relevant for static slugs like famous-and-special-restaurants.

Verification

After the fix, start the dev server at port 5184 and visit /en/famous-and-special-restaurants?city_id=1 on iPhone 14 viewport. Confirm:

  1. AD restaurants appear in the list (small “Ad” label in upper-right of card image).
  2. Total restaurant count matches production (https://web.hungryhub.com/en/famous-and-special-restaurants?city_id=1).
  3. No regressions on non-AD entries (existing cards unchanged).
  4. The new test passes: pnpm exec turbo run test --concurrency 2 --filter=@hh-pegasus/shared-api.