Birthday Add-On — Pegasus API Implementation Plan
Scope: Pegasus V5 API consumer only. The hh-server backend and Book-Bite partner-portal plans are separate documents.
Implementation target repository: hungryhub-team/hh-pegasus-monorepo.
Goal: Hide free birthday add-ons from the generic restaurant add-on catalogue using the explicit API indicator, while preserving existing detail, image, and availability behavior.
Canonical wire contract: src/HHPegasusMonorepo/superpowers/specs/2026-08-06-birthday-add-on-api-contract.md
Implementation reference: src/HHPegasusMonorepo/feature/birthday-add-on-pegasus-implementation.md
API surface
Pegasus consumes these existing V5 endpoints:
GET /api/v5/add_on_packages.json?restaurant_id=:id&is_visible_for_staff=false
GET /api/v5/add_on_packages/:restaurant_add_on_id.json?is_visible_for_staff=false
POST /api/v5/restaurants/:restaurant_id/find_available_add_ons.json
After the shared response camelizer, the relevant attributes are:
attributes.addOnType: "standard" | "birthday"
attributes.itemType: "items_list" | "items_image" | string
attributes.isFreeBirthdayAddOn: boolean
The generic catalogue must hide a row only when:
attributes.isFreeBirthdayAddOn === true
Do not infer birthday/free semantics from a zero price, menuType, menus.type, or image presence. Standard zero-price rows must not be hidden by this birthday rule.
Existing menu compatibility
For both birthday itemType values, hh-server intentionally returns the existing image-gallery contract:
attributes.menuType === "image"
attributes.menus.type === "image"
attributes.menus.data.img // [{ thumb, original }]
The current Pegasus detail flow selects the renderer from menuType, not from menus.type. Keep this behavior unchanged. Birthday item captions (birthday_item, display_name, and description) are not present in the V5 image payload; a structured items_list renderer is out of scope for this integration.
File ownership and implementation
| Responsibility | File | Status |
|---|---|---|
| Shared add-on type and birthday discriminator | packages/shared-type/src/AddOnPackage.ts | Implemented |
| Shared item/free helper | packages/shared-helper/addOnPackage.ts | Implemented |
| SSR catalogue filtering | packages/shared-service/src/package/getRestaurantAddons.ts | Implemented |
| Hydrated store filtering | packages/shared-store/src/restaurant/restaurantAddOnPackages.ts | Implemented |
| Store regression coverage | packages/shared-store/src/restaurant/__tests__/restaurantAddOnPackages.test.ts | Implemented |
| Existing detail/image compatibility | packages/pages-restaurants/src/detail/RestaurantBodyView.vue | Implemented |
The filter must be applied consistently in SSR and hydrated client state, must not mutate the API response, and must remain idempotent. Direct detail loading by an explicitly requested ID remains available; availability requests and server-side booking validation remain unchanged.
Verification checklist
- Generic catalogue hides rows with
isFreeBirthdayAddOn === true. - Premium birthday rows remain visible.
- Standard add-ons are not classified as birthday from price or menu shape.
- Direct detail flow retains the existing image renderer for
items_listanditems_image. - SSR and hydrated store use the same explicit indicator.
- Store regression tests cover the birthday free-row filter.
- Run repository typecheck and formatter.
- Verify against staging responses for list, detail, standard zero-price, and availability flows.
Focused regression command:
./node_modules/.pnpm/node_modules/.bin/vitest run packages/shared-store/src/restaurant/__tests__/restaurantAddOnPackages.test.ts
Out of scope
- hh-server migrations, booking rules, serializers, or partner CRUD.
- Book-Bite form layout and multipart payload construction.
- New V5 menu types or a structured birthday caption renderer.
- Controller-level Flipper gating in Pegasus; visibility is driven by the response indicator and the existing product rollout flow.