# Shop · Packages / Bundles — Parity Matrix

Canonical = React demo (`/private/tmp/Navagoo_MI_dev/navagoo-app/src`).
Ours = Yii2 (`frontend/`, `common/`, `api/`).

| # | Demo behavior | Demo ref | Our ref (file:line or —) | Status | Note |
|---|---|---|---|---|---|
| 1 | Two entities: ServiceBundle + SubscriptionPackage | types.ts:229-263 | common/models/Package.php (one entity ≈ Bundle) | partial | SubscriptionPackage entirely missing |
| 2 | Bundles surfaced as a tab on Services page | Services.tsx:131,148,364 | frontend/views/package/index.php (standalone page) | partial | Bundle exists as our Package, but separate IA |
| 3 | Packages (subscriptions) surfaced as a tab | Services.tsx:132,149,425 | — | missing | No sessions/subscription concept |
| 4 | Shop-scoped reads | Services.tsx:83-84,369,430 | PackageController.php:71 + checkOwnership | done | |
| 5 | Show-inactive toggle; auto-hide inactive from list | Services.tsx:135-138; selectors.ts:112-113 | — | missing | Both statuses always listed |
| 6 | active = `active!==false`; status badges | selectors.ts:106; Services.tsx:386,449 | Package.php:31-32; index.php:112,124-137 | done | Active/Archived chip + toggle action |
| 7 | Separate `hidden` (visible-in-app) flag | selectors.ts:108-109 | — | missing | Only active/archived |
| 8 | Bundle card shows member names joined " + " | Services.tsx:375,401 | index.php:193-205 (list, first 4 + "more") | done | Different presentation, same data |
| 9 | Bundle card list-price strikethrough + savings | Services.tsx:377,415-416 | — | missing | We show only effective price chip |
| 10 | Package card: N-sessions badge | Services.tsx:442 | — | missing | |
| 11 | Package card: per-session price line | Services.tsx:466-470; finance.ts:654 | — | missing | |
| 12 | Package card: auto-renew badge | Services.tsx:443-448 | — | missing | |
| 13 | Bilingual name | types.ts:233; Services.tsx:876 | _form.php:201-212 (MyMultiLanguageActiveField) | done | |
| 14 | Bilingual description | types.ts:234; Services.tsx:884 | — | missing | No description field at all |
| 15 | Image upload | Services.tsx:893 | _form.php:181-194 (filekit) | done | |
| 16 | Variants | Services.tsx:894 | — | missing | |
| 17 | Discount none/fixed/percent | finance.ts:43-51; Services.tsx:929-958 | _form.php:251-254,525-541; ShopService.php:16-23 | done | percent not clamped to 100 our side |
| 18 | VAT split (inclusive) before & after discount | finance.ts:32-33,67-70; Services.tsx:920-995 | _form.php:543-581 → /shop-service/calculate-vat | done | Server-authoritative VAT |
| 19 | Live savings amount/% feedback | Services.tsx:758-759,984-988 | — | missing | |
| 20 | Discount-zeroes-price warning | Services.tsx:760,989-991 | — | missing | |
| 21 | Name required to save | Services.tsx:765 | base/Package.php:105 | done | |
| 22 | Base price > 0 required | Services.tsx:766 | base/Package.php:89,105 | partial | Required+numeric but no `>0` guard |
| 23 | At least one service required | Services.tsx:763,767 | — | missing | servicesIDs is `safe`, not required |
| 24 | Sessions ≥1 / validity ≥1 | Services.tsx:814-815,1047-1050 | — | missing | No fields |
| 25 | Auto-renew + billing period (monthly/6mo/12mo) | Services.tsx:817,1067-1085 | — | missing | |
| 26 | Included-services multiselect | Services.tsx:1055-1064 | _form.php:286-301 (Select2) | done | |
| 27 | Attach free routines/add-ons (before/after) | Services.tsx:1089-1125 | — | missing | |
| 28 | Agents read-only (managed in Team) | Services.tsx:1127-1146 | _form.php:303-316 (editable Select2) | partial | We allow editing agents here |
| 29 | Disabled-until-valid Save + missing-fields hint | Services.tsx:762-767,1149-1153 | _form.php:146-152 (server errorSummary) | partial | Different validation model |
| 30 | Delete from catalogue | store.ts:1841,1846 | Package.php:74-77 (soft-archive) | done | Divergent: soft delete (safer) |
| 31 | Edit existing bundle/package | Services.tsx:808,819 | PackageController.php:158-209 | done | (Bundle only) |
| 32 | Bundle weekday availability (activeDays) | types.ts:241 | _form.php:327-336; Package.php:50-58 | done | |
| 33 | Date-range scheduling + runtime gate | — (not in demo) | Package.php:37-61; _form.php:339-363 | done | NEW on our side / richer |
| 34 | Package `period` = Σ service_period | — (not in demo) | PackageController.php:266-303; _form.php:58-83 | done | NEW on our side |
| 35 | Empty state | Services.tsx:370-371,431-432 | index.php:231-243 | done | |
| 36 | API exposure (bundle/package to mobile) | (store-driven) | api/.../LookupsController.php:132-148; BookingController.php:822+ | partial | Package read/booking exists; no bundle/subscription parity |

## New in demo (absent on our side)
- The entire **SubscriptionPackage** concept: sessions, validityDays, per-session
  price, auto-renew, billing period.
- **ServiceBundle as a distinct entity** with list-price/savings display.
- `hidden` (visible-in-app) flag separate from active.
- Bilingual **description**, **variants**, and **freebie (routine/add-on)
  attachments** on bundles/packages.
- Live discount **savings feedback** and **discount-zeroes** warning.
- "Show inactive" toggle with auto-hide of inactive rows.

## New on our side (absent in demo)
- Package **date-range scheduling** (`start_date`/`end_date`) + runtime
  `isScheduledActive()`.
- Auto-computed package **`period`** (Σ of member `service_period`).
- **Soft-delete (archive)** instead of hard delete.
- Editable **agents** assignment on the package form.

Area score: 45%

---

## Verified verdict (adversarial)

Each analyst "done"/high claim was re-checked by opening BOTH the demo ref and our
ref. Confirmed rows kept; overclaims downgraded with evidence.

### Downgrades (overclaims refuted)

| # | Feature | Was | Now | Evidence |
|---|---|---|---|---|
| 17 | Discount none/fixed/percent | done | **partial** | Demo (`Services.tsx:929-958`) is a 3-state radiogroup `none/fixed/percent` with `min={0}` and `max={100}` clamp on percent. Ours (`_form.php:251-254`) is a 2-option dropdown (`ShopService::discountType()` → only Fixed/Percentage); there is **no explicit "none"** and **no min/max clamp** client-side. Server (`ShopServiceController::actionCalculateVat` 200-203) computes `total*value/100` with **no >100 clamp**, and `base/Package.php:91` keeps `discount`/`discount_type` merely `safe`. A percent of 250 produces a negative price unchecked. |
| 18 | VAT split (inclusive) before/after discount | done | **partial** | The endpoint (`actionCalculateVat`) does return `before_discount` + `after_discount`, but the form JS (`_form.php:566-574`) only renders the **after-discount** subtotal/VAT/net into `#calc-price` — the before/after dual split the demo shows (`finance.ts:67-70`) is never surfaced. More importantly the claim "server-authoritative VAT" is misleading: `price`/`price_before` are `safe` only (`base/Package.php:91`), there is **no `beforeSave`/`beforeValidate` recompute** in `Package.php`. The saved `price` is whatever the readonly client field posts (`_form.php:264-269,574`); a crafted POST or JS-off client stores an arbitrary price. VAT is display-only, not enforced on persist. |

### Confirmed (claims that held up)

| # | Feature | Status | Note |
|---|---|---|---|
| 4 | Shop-scoped reads + ownership | done | `actionIndex` filters `shop_id` (`PackageController.php:71`); view/update/delete/toggle all call `checkOwnership` (`FrontEndController.php:120-139`). Holds. |
| 6 | Active/inactive badge + toggle | done | `actionToggleStatus` + index chip (`index.php:124-137`). Minor divergence: demo confirms only on deactivate; ours confirms both directions. |
| 8 | Bundle card member service names | done | `index.php:193-205` (first 4 + "+N more") vs demo `" + "` join — same data. |
| 13 | Bilingual name | done | `MyMultiLanguageActiveField` (`_form.php:201-211`). |
| 15 | Image upload | done | filekit `Upload` (`_form.php:181-194`). |
| 26 | Included-services multiselect | done | Select2 multi (`_form.php:286-301`). |
| 30 | Delete from catalogue | done | Divergent soft-archive (`Package.php:68-77`) — safer; acceptable. |
| 31 | Edit existing | done | `PackageController.php:158-209` (Bundle-equivalent only). |
| 32 | Weekday availability (activeDays) | done | `_form.php:327-336`; `Package.php:50-58`. |
| 33 | Date-range scheduling + runtime gate | done | NEW on our side — `Package::isScheduledActive()` 37-61. |
| 34 | Auto-computed period (Σ service_period) | done | NEW — `PackageController::actionGetPeriod/actionCalculatePeriod` 263-303. |
| 35 | Empty state | done | `index.php:231-243`. |

### Already-correct lower claims (spot-checked, unchanged)
- 22 (base price >0) stays **partial** — `base/Package.php:89,105` is `number`+`required`, no `>0` guard; demo gates on `baseNum > 0` (`Services.tsx:766`).
- 23 (≥1 service required) stays **missing** — `servicesIDs` is `safe` (`base/Package.php:91`); demo blocks save via `needsServices` (`Services.tsx:763,767`).
- 28 (agents read-only) stays **partial** — ours editable (`_form.php:303-316`).
- 29 (disabled-until-valid save) stays **partial** — server errorSummary, not live gating.
- 36 (API parity) stays **partial**.
- 1,2,3,5,7,9,10,11,12,14,16,19,20,24,25,27 unchanged (partial/missing as filed).

### Adjusted area score

Two "done" rows (17, 18) demoted to partial. The remaining confirmed rows hold, but
the discount/VAT subsystem — one of the analyst's strongest "done" claims — is in
practice **display-only with no server enforcement**, which is a material parity gap
for a money surface. Net effect on the area is small (two of ~12 done rows drop a half
step) but the qualitative confidence in the pricing path is lower than the matrix implied.

**Adjusted area score: 42%** (was 45%).
