# Customer · Discover — Business Rules

Rules the demo Discover screen encodes, as implementable statements, each mapped to
our API. (Demo = `portals/customer/Discover.tsx` + `store/seed.ts` + `types.ts`.)

1. **Only `active` shops are discoverable.** Inactive / onboarding / pending-auth
   shops MUST be excluded from the listing.
   - Demo: `Discover.tsx:25` (`status === 'active'`).
   - Ours: `ShopsController.php:59` & `:148` (`status = Shop::STATUS_ACTIVE`). ✅

2. **Listing is scoped to a single data world.** A user only sees shops belonging to
   their environment (demo vs live).
   - Demo: implicit single in-memory world.
   - Ours: `is_demo` partition keyed off the authenticated user
     (`ShopsController.php:60-64`, `:149-153`). ✅ (ours stronger).

3. **Rating is a real aggregate, not per-screen mock.** A shop exposes an average
   rating and a total-ratings count.
   - Demo: MOCK (`RATINGS` map, `Discover.tsx:9-13`); fallback `4.8 / 0 reviews`.
   - Ours: real `rate` + `total_rates` (`ShopsResource.php:23-30`). ✅ (ours correct).

4. **Distance is computed from the user's coordinates.** Shown in km, used to order
   "near you".
   - Demo: MOCK strings (`'1.2 km'`, `Discover.tsx:10`).
   - Ours: Haversine within a configurable range, ordered by distance
     (`Shop.php:91-149`; field `distance` `ShopsResource.php:117`). Requires
     `lat`/`long` params; absent → no distance/order. ✅ when coords supplied.

5. **Gender/type filtering is inclusive.** Male filter → male + unisex; Female →
   female + unisex; Both/All → everything; missing → fall back to the user's profile
   gender.
   - Demo: only a display label (`TYPE_LABEL`, `Discover.tsx:15-19`); no filtering on
     this screen.
   - Ours: `Shop::applyGenderFilter` (`Shop.php:239-272`). ✅ (ours ahead).

6. **"Services count" and "from {minPrice}" summarise a shop's catalogue.**
   - Demo: count of services for the shop; min `service.price`
     (`Discover.tsx:120-123`). `price` is the post-discount price (`seed.ts:884-896`).
   - Ours: service price + `price_before_discount` exist per service
     (`ShopServiceResource.php:19-25`) but there is **no aggregated count/min-price on
     the shop list item**. ⚠ partial.

7. **Min price reflects the discounted price.** When a service has a discount, the
   card "from" value uses the effective (post-discount) price, while a before/after
   split is available.
   - Demo: `price` already post-discount; `basePrice` holds the original
     (`seed.ts:881-896`).
   - Ours: `price` = `service_amount`, `price_before_discount` = `service_amount_before`
     when > 0 (`ShopServiceResource.php:14-25`). ✅ (concept matches).

8. **Booking is not initiated from Discover** (later milestone). Discover only
   surfaces shops.
   - Demo: `toast.info('Shop page coming soon')` (`Discover.tsx:79`).
   - Ours: booking is a separate API (`BookingController.php`); Discover/shop list is
     read-only. ✅ (consistent).

9. **Deals are promotional offers with a discount.** Each deal has: description,
   discount type (`percent`|`fixed`), discount value, usage cap, usage count, expiry.
   - Demo: `seed.ts:1136-1167` (`deals` array); `types.ts` Deal shape.
   - Ours: NO single deals entity. Promo codes carry value in booking
     (`BookingController.php:908-909`); Ads carry only an image
     (`AdsResource.php:8-19`). ❌ missing as a discoverable feed.

10. **Deals may be shop-specific OR platform-wide.** A deal with no `shopId` applies
    to all salons and is labelled/iconed differently.
    - Demo: `deal.shopId == null` ⇒ "Platform-wide · all salons" + Sparkles icon
      (`Discover.tsx:91-104`).
    - Ours: Ads are always shop-scoped (`AdsController.php:32-34` inner-joins shop);
      no platform-wide promo concept. ❌ missing.

11. **Promo banners (Ads) are gated to active shops.** A banner from an inactive
    shop must not appear.
    - Demo: deals reference active shops in seed; no explicit gate on the screen.
    - Ours: `AdsController.php:32-35` joins shop and filters `status = ACTIVE`. ✅
      (ours encodes the rule explicitly).

12. **Localisation.** Listing/labels should be available bilingually (ar/en).
    - Demo: Discover screen strings hard-coded English (not i18n-routed here).
    - Ours: `?lang=ar` switches language (`AdsController.php:19-25`); resource fields
      resolve localized names. ✅ (ours ahead).
