# Navagoo parity audit — cross-portal summary

**Baseline:** Navagoo 2.0 mockup, branch `dev`, commit `cb48c8d` (v0.28.0 — "aura login + shop
first-run setup wizard")
**Implementation:** `stageadmin.navagoo.com` (admin · `backend/`) and `stageshops.navagoo.com`
(shop · `frontend/`), with the local checkout `5efbafc9` read as corroboration
**Date:** 2026-08-06 · **Phase C close-out**

This is the summary spanning both portals. It does **not** restate per-portal detail — that lives in
[`REPORT_ADMIN.md`](REPORT_ADMIN.md) and [`REPORT_SHOP.md`](REPORT_SHOP.md), with ticket-ready items
in [`BACKLOG_ADMIN.md`](BACKLOG_ADMIN.md) and [`BACKLOG_SHOP.md`](BACKLOG_SHOP.md). What is here is
the picture across the whole system: the findings that live in shared code, the ones that collapse
into a single root cause once both portals are read together, the ordering constraints between
fixes, and the decisions that are waiting on a person rather than on engineering.

It is written to the register in [`00_SPEC/PARITY_AUDIT_SPEC.md`](00_SPEC/PARITY_AUDIT_SPEC.md) §11:
observed state versus baseline, never attribution.

---

## 1. What was compared, and how

Two applications were compared against one pinned mockup commit.

| Side | Ref | Verification |
|---|---|---|
| Mockup | `Navagoo 2.0`, `dev`, `cb48c8d` (v0.28.0) | `git fetch` succeeded · 0 ahead / 0 behind `origin/dev` · working tree clean, 2026-08-06 |
| Portal (local) | `NavagooBackend`, `parity-audit` off `tailwind-poc`, `5efbafc9` | pulled 2026-08-05 |
| Portal (staging) | `stageadmin.navagoo.com` · `stageshops.navagoo.com` | sign-in verified 2026-08-06 on both; build fingerprinted by served-asset metadata |

Neither deployment exposes a version string, so the build is fingerprinted by asset metadata — the
shop Tailwind bundle was built on staging **2026-08-03 17:41:58 GMT** (104,719 bytes). Six commits
sit between that build and local `HEAD`; five are `api/` tier work, out of scope, and the sixth is
dated the same day as the build. **Staging is a sound proxy for local `HEAD` for the two portals
graded here** — with the standing limitation that asset timestamps bound a build, they do not prove
it, and PHP on staging could be newer or older than the CSS bundle. Findings are reported against
observed staging behaviour with local code read as corroboration, never the reverse.
Full record: [`02_EVIDENCE/environment.md`](02_EVIDENCE/environment.md).

### The method, in brief

1. **Expectations were pre-registered from the baseline before the implementation was opened.**
   164 behavioural contracts (82 admin, 82 shop — 165 area-level evaluations, because one admin
   contract sits in two areas) and 252 baseline surfaces (101 admin, 151 shop) were extracted from
   mockup source first. A 37-item demo-artifact exclusion list was agreed with the product owner up
   front, so nothing that exists only as demo scaffolding could be reported as a gap.
2. **Contracts were verified against source**, area by area, with file:line evidence, and the
   verbatim `Baseline` / `Observed` pair preserved in each area file.
3. **An adversarial refutation pass** ran over every candidate finding. Ten candidates were refuted
   and dropped — 3 admin, 7 shop — including three where the *stated baseline* was shown to be a
   misreading of the mockup rather than the portal being wrong.
4. **Every S1 was then attacked by an independent hostile pass** demanding four things before the
   finding could stand: quoted code from the cited files, evidence the path is reachable in
   production, a check against the mockup baseline, and a concrete failure scenario with numbers.

That last pass **changed 8 admin findings and 4 shop findings** — downgrades, merges and in-place
corrections. The recorded outcomes are: admin **7 upheld, 0 refuted outright, 2 downgraded to S2
(F-FIT-02, F-FIT-03), 2 merged into 1 (F-FIT-01 + F-FIT-05)**; shop **11 upheld, 0 refuted outright,
2 downgraded, 3 merges**. Three admin S1s additionally carry a stated correction or caveat that must
travel with them, and one shop finding's evidence was rewritten because it contradicted another
finding about the same money.

**This is stated because it is the audit's own quality evidence.** A finding set that survives a pass
designed to break it is worth more than one that was never tested, and the changes show the bar
biting in both directions — toward the portal and away from it.

---

## 2. The headline answer

**Coverage is broad. Divergence is narrow and deep, and it concentrates in the wiring between
configuration and the money engines.**

On both portals, every pre-registered contract had an implementing code path to evaluate against —
164 of 164. **No baseline screen was found absent from either portal.** Admin navigation IA matches
the mockup item-for-item; the shop portal's screen-level IA, tab sets, filter toolbars and status
vocabulary match the baseline. The design system was ported with real fidelity: every design token
sampled on the shop portal is byte-identical to the mockup (14 of 14, including the full status-badge
ramp), and on admin the only token-level divergence found across the whole sample is a CTA corner
radius. RTL mirrors structurally correctly on both.

Where the two portals diverge, they diverge in two recurring places:

- **Between an admin-editable configuration value and the engine that should read it.** A field
  exists on a screen, saves to a column, and no consuming code path reads it. The screen reports
  success; the engine uses a different value. Nine admin findings and ten shop findings share that
  exact shape, and it accounts for three of the seven admin S1s.
- **Between two rails that both exist and disagree.** A newer `charge` ledger implements the
  baseline's formulas; a legacy `Earnings` / `shop_earning` engine is what the shop is shown and paid
  on. On one navagoo-sourced booking the same Earnings screen shows Navagoo fees of **19.67** in its
  tile and **23.00** in the row beneath.

Neither the pass rate nor the severity count carries that answer alone, and they do not order the
work the same way. The pass rates — **admin 20.5%, shop 30.5%** — measure how many contracts match
the baseline *exactly*; the dominant verdict on both portals is `partial`, which is broad coverage
with lower exactness rather than absent features. The severity counts weight consequence. Areas
scoring 0.0% carry no S1 at all, and areas scoring above the overall rate carry two. **Severity, not
pass rate, should drive remediation.**

Stated plainly, without inflating or softening: this is predominantly a body of code that exists and
runs and diverges at specific decision points, not a body of unbuilt features. Across both portals,
**66% of confirmed findings are deviations** and **46% are `built-not-working`**; `not-built` work is
28%.

---

## 3. Combined scorecard

Both portals side by side. The two tracks are scored separately per spec §4.3 and are never averaged
together.

### Track L — behavioural contracts

| | Admin | Shop | Both |
|---|---:|---:|---:|
| Contracts pre-registered | 82 | 82 | 164 |
| Area-level evaluations | 83 | 82 | 165 |
| pass | 17 | 25 | 42 |
| fail | 16 | 21 | 37 |
| partial | 50 | 36 | 86 |
| needs-probe / unverifiable | 0 / 0 | 0 / 0 | 0 / 0 |
| **Pass rate** | **20.5%** | **30.5%** | 25.5% *(derived)* |
| S1 | **7** | **11** | **18** |
| S2 | 38 | 27 | 65 |
| S3 | 33 | 17 | 50 |
| S4 | 9 | 5 | 14 |
| **Confirmed findings** | **87** | **60** | **147** |
| Refuted and dropped | 3 | 7 | 10 |
| Live probes recommended, not run | 30 | 35 (over 31 contracts) | 65 |

The combined pass rate is the arithmetic sum of two independently scored runs and is shown for
completeness only. It is not a system-wide quality index, and neither portal's figure should be
quoted as one — §2, and each portal report's own "how to read" section, state why.

### Track U — UI parity

| | Admin | Shop |
|---|---|---|
| Baseline surfaces inventoried | 101 (18 screens · 30 tabs · 38 modals · 1 drawer · 14 panels) | 151 (20 screens · 24 tabs · 45 modals · 6 drawers · 56 panels) |
| Surfaces compared on both sides | 12 primary landing screens (one per area) at 1440, plus one RTL sample | 51 (14 screens · 12 tabs · 9 modals · 2 drawers · 14 panels) at 1024 / 1280 / 1440 / 1920, EN + AR |
| Findings | 18 — 0 S2 · 8 S3 · 10 S4 | 29 as recorded — 1 S2 · 17 S3 · 11 S4 (32 as enumerated: 1 S2 · 20 S3 · 11 S4) |
| Baseline screens absent | none | none |
| Design tokens | identical on every value sampled except one CTA radius | identical — 14 of 14, no exception found |

The shop report records a counting discrepancy inside its own UI source between the headline total
(29) and the enumerated set (32); both figures are carried, `BACKLOG_SHOP.md` holds all 32 items, and
the severity *shape* is the same on either count. It is a re-count task, not a re-observation.

**On both portals the UI dimension is materially stronger than the behavioural one** — 47 UI findings
with a single S2 between them, against 18 S1 and 65 S2 behavioural findings.

### Backlog

`BACKLOG_ADMIN.md` carries **112** items (94 Track L + 18 Track U); `BACKLOG_SHOP.md` carries **92**
as recorded (95 as enumerated). Together **204** (207 enumerated) — but **not 204 independent pieces
of work**; see §5.

---

## 4. The 18 S1 findings, grouped by shared root cause

Grouped by cause rather than by portal, because that is how they will be scheduled. Several collapse:
18 findings reduce to **10 distinct root causes**. One line each — the evidence, the worked example
and the caveats live in the per-portal reports and backlogs.

### A · An admin-set value is stored, editable and persisted — and unread · 3 findings · admin

| Finding | One line |
|---|---|
| CF-CC-01 | The carry-forward threshold the admin edits has no reader; the resolver falls through a column that does not exist on `settings` to a hardcoded `200.0`. *(The invoice amount itself is computed correctly — the wrong numbers are the credit limit and the timing.)* |
| CF-CC-02 | `OfferPricingService::rateDiscountPct()` is complete and unit-tested and has **no caller**, so a fee-discount offer never reduces a stamped rate. |
| NOTIF-02 | A plan's `sms_included` / `wa_included` allowance is admin-editable and is read nowhere at billing time; the resolver takes no shop or plan argument. |

### B · The console tier hand-mirrors the controller's billing methods · 2 findings · admin

| Finding | One line |
|---|---|
| SE-02 | The renewal cron charges `priceForPeriod()` gross while the subscribe path applies `netFor()`, so an enrolled offer's discount is honoured on charge one and lost from charge two onward. |
| SE-03 | The renewal path's dev fallback writes a subscription charge `paid` when no card is on file, and is not gated by any dev/prod flag. |

*The two rails can disagree at all because the net-price lookup was not carried into the mirror and
the simulation rail was. Extracting one billing service used by both paths closes both and removes
the mechanism by which they can diverge again.*

### C · No bidirectional link between the settlement rail and the fee rail · 1 finding · admin

| Finding | One line |
|---|---|
| F-FIT-01+05 | Charges are not tagged with a transfer id and earnings are not marked consumed by an issued invoice, so the same money can be recovered on the invoice rail and paid out on the transfer rail. *(Merged from two ids on re-verification precisely because reporting them separately double-counts one exposure.)* |

### D · The marketing fee runs on the legacy rail with no classification gate, while the ledger rail stays empty · 2 findings · one per portal

| Finding | One line |
|---|---|
| FIN-LEDGER-01 (admin) | No completion or no-show transition invokes `deriveBookingCharges()`, so no `marketing_fee` ledger row is ever raised; the staging ledger contains none at all. |
| F-FIN-02+03 (shop) | The same absence, plus the legacy `Earnings::calculateNavagooMarketingFees` charging `platform_commission%` on every completed booking with **no classification check, no minimum floor and no grace waiver** — and that column is what the shop is shown and paid on. |

*Both phases agree on the caveat: remediation must target the charge ledger and must not be pointed
at the payout rail, which nets a marketing fee from the legacy column.*

### E · No portal path asks whether a booking is a package redemption · 4 findings · shop

`Booking::getIsPackageRedemption()` exists with **zero call sites repo-wide**;
`PackageEntitlement::reinstateSession()` and
`FinanceLedgerService::reversePackageRedemptionCharge()` exist, are correct, and are called **only
from the `api/` tier**.

| Finding | One line |
|---|---|
| PKG-01+02 | A marketing fee is stamped at redemption and a second on a portal cancel — **16.96 against a baseline 0.00** on one visit. Two code sites; fixing either alone leaves the other live. |
| PKG-03 | A completed redemption reports its booking value as revenue **and** mints withdrawable settlement credit for money never collected — 800 against a 760 sale. |
| PKG-05 | A shop cancel destroys a paid session, leaves the customer **190.00** of pre-paid entitlement short and the redemption fee standing. Classified as data loss. |
| CF-CC-02 (shop) | `outstandingBalance` has no redemption branch, so a pre-paid visit cannot be completed without recording in-store money that was never taken. |

*This is not four features to build: it is one guard at the portal's write paths plus two existing
helpers to call. The same customer, the same package, two different outcomes depending on who
cancels.*

### F · The walk-in record is created without a payment split · 2 findings · shop

| Finding | One line |
|---|---|
| F-FIN-01 | `amountCollected()` falls through to `total_amount` when `amount_collected` is NULL, so a portal-created pay-on-visit booking reads as fully collected; the same helper feeds refund, payout and settlement. |
| CF-CC-04 | The collect path therefore stamps a **non-refundable 19.67** processing fee on cash taken at the counter. |

### G · The payout is built from the legacy column and never subtracts processing fees · 1 finding · shop

| Finding | One line |
|---|---|
| F-FIN-10 | `net_transferable_amount` accumulates `net_collectible_amount` and subtracts only notification fees — **19.67 overpaid** per eligible online booking (19.96 with a non-booking invitation fee). **Read §6 before scheduling this.** |

### H · The create-then-pay-now step fails silently · 1 finding · shop

| Finding | One line |
|---|---|
| CF-CC-05 | The new-walk-in Pay-now sub-step posts to `/booking/collect` against a SCHEDULED booking, the FSM guard rejects it with `success:false`, and the client resolves both the `.then` and the `.catch` to "saved" — the collection and the card tip are discarded with no error. |

### I · Booking creation never runs the placement check · 1 finding · shop

| Finding | One line |
|---|---|
| BE-F01 | `checkPlacement` is called on the reschedule and reassign paths and never from `WalkInBookingService`, so time-off, working-hours containment and can-perform are unenforced at creation. |

### J · The inverse business-minute conversion never rolls the calendar date · 1 finding · shop

| Finding | One line |
|---|---|
| BE-F03 | On an overnight business day a post-midnight minute is paired with the business-day date, so the row lands on the previous night's session, is skipped by the overlap scan, and the slot is offered again. *(The similarly-collapsing `fromBusinessMinutes()` is dead code — not the site to fix.)* |

---

## 5. Cross-cutting items — fix once, serve both

A material share of the work is in `common/`. `BACKLOG_SHOP.md` marks these **[→ ADMIN]** and
cross-references rather than duplicating: **at least eleven shop items are the same shared code as an
admin item** (thirteen rows in that table, some pairing one shop item to two admin items).
Deduplicating those, the shop phase adds roughly **80 new Track L items** to what the admin phase had
already registered — not 92 on top of 112.

The severity often differs between the two portals because the *observable consequence* differs, not
because the code differs. Both gradings are kept:

| Shared code | Shop | Admin |
|---|---|---|
| `FinanceLedgerService::amountCollected()` falling through to `total_amount` | F-FIN-01 + CF-CC-04 · **S1** | FIN-LEDGER-04 · S2 |
| No completion transition invokes `deriveBookingCharges()` | F-FIN-02+03 · **S1** | FIN-LEDGER-01 · **S1** + FIN-LEDGER-02 · S2 |
| `deriveBookingCharges()` has no package-link guard | PKG-01+02 · **S1** | FIN-LEDGER-07 · S2 |
| `OfferPricingService::rateDiscountPct()` has no caller | F-FIN-06 · S2 | CF-CC-02 · **S1** |
| `freeLimitFor()` takes no shop or plan argument | SN-01 · S2 | NOTIF-02 · **S1** |
| A package sale raises fee rows but no `ShopEarning` | PKG-04 · S2 | F-FIT-13 · S3 |
| `buildPackagePurchaseCharges()` never consults the grace window | PKG-07 · S2 | FIN-LEDGER-08 · S3 |
| Cancelled and no-show bookings create no `ShopEarning` | F-FIN-07 · S2 | F-FIT-06 · S2 |
| Payment-method availability ignores the plan's gating feature | SN-04 + CF-CC-06 · S2 | SE-06 · S2 |
| `shopCanAccess()`'s permissive `$defaultAllow` | SN-03 · S3 | SE-05 · S2 |
| Per-channel approval columns have no reader | SN-05 · S3 | NOTIF-03 · S2 |
| Three `walkin_*` columns dropped by `scenarios()` | CF-CC-07 · S3 | SE-07 · S3 |
| No scheduled billing / threshold pass | F-FIN-11 · S3 | F-FIT-09 + F-FIT-12 · S3 |

**Three further cross-cutting observations:**

- **One absence produces six admin findings across four areas.** `AuditLogService::emit()` writes
  actor / scope / shop / action / before / after into `audit_log`, is registered as a component, and
  has **zero call sites** in any of the five tiers. The Events screen reads a different table, written
  by four model hooks in signup and shop flows only. That is one wiring task with unusually broad
  coverage.
- **The notification-dispatch gap is carried once, on the admin side**, as H7 and open question
  **OQ-NOTIF-B** ("can a shop be billed for a message that was never sent?"). The shop phase re-tested
  it unchanged and added a precision that must travel with it: **provider gateways do exist and are
  wired elsewhere** — Msegat/Twilio for OTP, the T2 WhatsApp API for invitation campaigns — so the gap
  is specific to the notification-trigger dispatch path, not to the platform's messaging capability.
- **One shop-graded finding's fix lands in admin-tier code.** F-FIN-09 cites only
  `backend/controllers/AdminFinanceController.php`; the consequence graded is shop-facing. Whether it
  is one ticket with the adjacent admin item F-FIT-13 was not assessed.

**On the `api/` tier, which is out of scope.** Several correct implementations live there — the
package reinstate/reverse path, `amount_collected` assigned explicitly at creation, `recordRedemption`.
**Their existence is evidence about where the working code is, not a finding about the api tier**, and
it is a scheduling fact: in group E the working implementation already exists in the tier this audit
does not grade.

---

## 6. Remediation-ordering constraints

Some of these fixes interact. Fixing one alone makes another worse, or makes a real fix look like a
no-op.

### 6.1 F-FIN-01 + F-FIN-10 must ship together — the sharpest case

| | F-FIN-01 (spurious walk-in processing fee) | F-FIN-10 (payout omits processing fees) |
|---|---|---|
| What it does today | Stamps a **19.67** processing fee on a walk-in where nothing was collected on the rail | Never subtracts any processing fee from `net_transferable_amount` |
| Where it lands today | Displayed figures and the invoice rail | The actual payout |
| Net effect on cash | **None** — the spurious fee is never deducted from what the shop is paid | **19.67 overpaid** per eligible online booking (**19.96** with a 0.29 non-booking invitation fee) |

**The spurious fee reaches only screens and invoices *because* the payout rail does not read
processing fees at all.** Correct the payout rail on its own and the fee stops being a display error
and becomes a deduction: **every walk-in begins costing the shop 19.67 it does not owe.**

The safe ordering is F-FIN-01 **before or in the same release as** F-FIN-10. Fixing F-FIN-01 first is
harmless — it only corrects displayed and invoiced figures. **A fix to F-FIN-10 verified in isolation
will look correct**, because the payout arithmetic will match the baseline formula, while producing a
new overcharge on exactly the booking type the shop portal creates most.

### 6.2 The other coupled pairs

- **F-FIN-02+03 — fixing either side alone leaves the shop on a wrong number.** Raising the
  `marketing_fee` ledger row does not change what the shop is paid, because the payout nets the
  legacy `navagoo_marketing_fees` column; ungating the legacy engine does not correct the ledger.
  Both phases record the same caveat, from opposite directions.
- **PKG-01+02 — two code sites, both required.** The redemption-time stamp and the cancel-time stamp
  are independent; closing one leaves the other live.
- **F-FIT-01+05 — one link, two directions.** Tagging charges without marking earnings consumed (or
  the reverse) closes half the leak.
- **SE-02 + SE-03 — one structural fix, or two symptom fixes that leave the duplication in place.**
  Extracting a shared billing service closes both; patching each symptom inside the mirrored console
  method leaves the mechanism that produced them.
- **CF-CC-04 is F-FIN-01 seen from the collect path.** The walk-in create-path assignment closes
  both.

### 6.3 A verification hazard that applies to every finance fix

**Commercial rates read zero across staging for most of this audit.** Every fee on the pre-existing
ledger resolves to 0.00, so a probe of any fee returns 0.00 regardless of whether the formula is
right — **and a fix verified against zero rates will look like a no-op.** Rows already written carry
their stamped rate, so a probe must create **fresh** records after rates are configured, not re-read
old ones. Every backlog item carries its own precondition line (`P-RATES`, `P-OFFER`, `P-PKG`,
`P-OVERNIGHT`, `P-WALKIN`, `P-SMSPRICE`, `P-PLANBUNDLE`, `P-NOSUB`, `P-NOTIFCHG`, `P-CRON`,
`P-MARKETING`) for this reason.

---

## 7. Decisions waiting on the product owner

**This is the most actionable section in the document, because none of it is blocked on engineering.**

[`03_FINDINGS/_open-items.md`](03_FINDINGS/_open-items.md) consolidates **151 open items** from the
repository's own planning documents. **57 carry `🅿 product`** — they cannot move without an owner
ruling. The four below are now load-bearing under a confirmed S1 or S2 and are the ones to settle
first.

### 7.1 The marketing-fee basis · `OI-FIN-11`

`NVG_BEA_005_ATTRIBUTION_FREEZE_PLAN.md:45` records the deviation and explicitly leaves it open:
**the BRD says 5% × Net Collected (excl. VAT); the live code uses booking value excl. VAT.** It was
recorded as "NOT changed — needs product confirmation".

**Why it is now load-bearing.** F-FIN-01 (S1) and FIN-LEDGER-04 turn on exactly which number the fee
basis reads: `amountCollected()` falls through to `total_amount` when `amount_collected` is NULL.
Deciding the basis decides what "correct" means for that fallback — and therefore what the fix is.
Adjacent: `OI-FIN-12` (where the online-payment fee is stamped — the nominal stamping point lives in
the out-of-scope `api/` tier, and the fee stamps at completion or cancel instead) is the same
question from the other end, and is traced directly to CF-CC-04 (S1).

### 7.2 Whether to flip the permissive entitlement default · `OI-SUB-05`

`EntitlementService::shopCanAccess()` takes `$defaultAllow = true` and returns it when a shop has no
subscription row. It is **documented in code as a deliberate live-rollout posture** so shops
predating billing are not locked out of features already in use, and it is narrowed by a `GO_LIVE_MAP`
that still blocks bookings for such a shop. Recorded as SE-05 (S2, admin) and SN-03 (S3, shop).

**The decision is narrower than the planning documents imply.** They record entitlements as deferred
with "no enforcement"; enforcement has since shipped and was **verified working live** (§9). The open
question is whether to flip a one-line default — a hide-vs-lock call — not whether to build a gate.
The no-subscription branch could not be verified end to end: no portal credentials existed for a shop
with no `shop_subscription` row.

### 7.3 The multi-service package fee basis · `OI-CAT-01`

`NVG_BEA_008_SERVICE_BUNDLES_SUBSCRIPTION_PACKAGES_PLAN.md:21` records the BRD as ambiguous on the
marketing-fee basis for a multi-service package redemption; the build proceeded on
`price ÷ sessions_total` and the question was flagged to a named person.

**Why it is now load-bearing.** The packages area carries **four S1s** and the highest defect density
in the audit — 8 confirmed findings against 9 contracts, with no S3 or S4 at all. PKG-08 records the
redemption being valued from the live catalogue price rather than the snapshotted
`per_session_price`, and PKG-01+02 records a second marketing fee stamped at redemption. **The basis
ruling determines what the correct redemption value is**, so it should precede the PKG fixes rather
than follow them. Adjacent IA ruling: `OI-CAT-02` (packages-vs-bundles), which is also the UI face of
the shop portal's single Track U S2.

### 7.4 The role vocabulary · `OI-OTH-06`

`ADMIN_PARITY_PROGRESS.md:27` flags "7-role vs 4-RBAC, needs sign-off". The audit found four staff
roles against the baseline's seven — no `finance`, `support` or `front_desk` — sixteen menu
permission keys absent from the grantable catalogue, and a custom role yielding a login with no
reachable page (F-RBAC-01, F-RBAC-03, F-RBAC-04, F-RBAC-05). `system-rbac` carries **no S1**, but it
scores **0.0%** on contract pass rate: every contract in it diverges in some detail. **The role
vocabulary is the decision the rest of that area's work depends on** — the permission catalogue and
the default grant grid cannot be settled before the role set is.

### 7.5 Also owner-only, and not blocked on anything

Three infrastructure actions no engineering decision can substitute for: **`OI-OTH-01`** rotate the
committed Firebase service-account key (needs a force-push window with a named person),
**`OI-OTH-02`** remove `Options Indexes` from the storage vhost — it gives a public directory listing
of uploaded bank slips — and **`OI-OTH-03`** the Google Maps key referrer/billing configuration.
**`OI-SUB-01`** (provision production Paymob credentials) belongs with them: H6 established that
subscription checkout takes a documented dev fallback when the keys are absent, and **the same
fallback would silently mark production charges paid if the `PAYMOB_*` keys were ever absent there.**

Also worth an early ruling because each sits under a graded finding: `OI-FIN-01` (the card path marks
an invoice paid with no gateway call), `OI-FIN-02` / `OI-FIN-03` (in-store tips excluded from payout;
the group outstanding-balance formula), `OI-SUB-06` (whether a failed subscription payment
auto-deactivates), and `OI-OTH-07` (the three walk-in payment toggles that render, save without error
and change nothing — wire or remove).

### 7.6 Five ambiguities in the baseline itself

Recorded in `01_BASELINE/shop/contracts.md`: **five internal inconsistencies were found in the mockup
itself.** Where the portal resolved one of them differently, the honest classification is *"the
specification was ambiguous"*, not *"the implementation is wrong"*. These belong in a conversation
with the product owner rather than in a remediation backlog.

---

## 8. Documentation currency

[`03_FINDINGS/_stale-docs.md`](03_FINDINGS/_stale-docs.md) reconciles the repository's own planning
documents against what the audit observed. **34 entries.**

| Direction | Count | Meaning |
|---|---:|---|
| `open → done` | **24** | The document records the item as deferred, missing or not built; the audit found it implemented. |
| `done → open` | **4** | The document records the item as complete, verified or benign; the audit found otherwise. |
| `changed` | 6 | The document describes an environment, path or baseline that has since moved. |

**The direction is worth stating plainly: the documentation understates completion far more often
than it overstates it — by six to one.** Capabilities recorded as deferred and found built include
the entitlement engine and its globally attached filter, the renewal cron (scheduled daily on qc and
prod), proration, admin offers CRUD, the charges ledger and shop-balances screen, category-tree
nesting, the `SubscriptionPackage` / `PackageEntitlement` models, the group-bookings feature
(recorded as "genuinely 0% implemented"; it is a graded area with 11 contracts), the refund-zone
engine wired into the cancel flow, and the highest-priority "fix first" backlog item, which is closed
and names itself in an in-code comment.

The practical cost of that direction is that work gets scheduled against a picture that is out of
date, and that several documents route a reader somewhere that does not exist: a `docs/parity/<key>/`
tree that is not in the working tree, a demo-repo path that does not resolve on this machine, a
mockup dev-server port (`5173`) that `strictPort: true` guarantees will never answer, and a parity
scorecard cut against a superseded mockup ref (`42dc101`) that still reads as the current position.

### The four reverse cases

Each is named because each records something as verified, benign or complete where the audit found a
defect — the direction in which stale documentation is most costly.

| Entry | Document states | Audit observed |
|---|---|---|
| **D-30** · `DEMO_SYNC_V14_V20_MASTER_PLAN.md:16` | `shopBalanceView.runningBalance` is a stub — "**UI ignores it — not wired, no wrong number**" | F-FIN-08 (S2): the field has consumers — the dashboard balance panel and the settlement strip — and the clamp suppresses a negative (owed) position the shop is entitled to see. |
| **D-31** · `DEMO_SYNC_V14_V20_MASTER_PLAN.md:16` | Routing the admin balance through `shopBalanceView` is a tidy-up — "**a parallel calc — agrees on components, no user-visible divergence**" | F-FIN-09 (S2): the parallel calc sums `booking.total_amount` rather than what was collected, overstating the collectable side for every pay-on-visit and part-collected booking. The consolidation is a correctness fix. |
| **D-32** · `ADMIN_PARITY_PROGRESS.md:59` | Pass-4 functional row — "✅ Admin Commercial Config save → NEW charge uses new rate — **PASS**", with precedence per-shop → global → default | The global-config half holds. The **per-shop half does not**: F-ADJ-04 (S2) — the ledger reads `shop.platform_commission` while the form field *labelled* "Marketing Fee Rate %" writes a separate column that no fee, earnings, settlement or invoice path reads. Narrow the ✅ to the global path. |
| **D-33** · `DEMO_SYNC_V14_V20_MASTER_PLAN.md:19` | "P3 RBAC / staff logins: ⬜ **not started**" | Filed in this direction because **both** halves are misstated: role UI, a 25-key grantable catalogue and a `shopManager` role exist, *and* the remainder is a different shape than "not started" implies — four roles against seven, sixteen ungrantable menu keys, and a custom role that yields a login with no reachable page. |

The file also records what was **checked and upheld**, so those are not re-litigated — including the
note that `SMSHelper` / `WhatsAppHelper` are intentionally not wired into dispatch, which the audit
confirmed.

---

## 9. What is working

Reported with the same rigour as the defects. An audit that only lists problems is not an audit.

- **The design system was ported faithfully, on both portals.** Every colour, font and type-scale
  value sampled on the shop portal is byte-identical — **14 of 14**, including the complete
  status-badge ramp on both foreground and background. On admin the four status badge pairs match
  **to six decimal places**, and the only token-level divergence found anywhere in the sample is the
  primary CTA corner radius. Where the tokens *live* differs (inline custom properties in the mockup;
  compiled into the Tailwind bundle in the portal), the rendered result is identical — which passes
  the design-system bar and is not recorded as a finding.
- **Admin navigation IA is at full parity** — 19 nav items, same order, same section grouping as the
  baseline's `ADMIN_NAV`, with one label difference. *(Two qualifications travel with it: the section
  headers are themselves permission-gated, so a `manager` session sees a flat list; and Settings
  renders inside the System group rather than the footer slot.)* The shop portal's nav matches
  including the footer treatment.
- **RTL is structurally correct on both portals.** No layout mirroring defect was observed anywhere,
  on either side. Shells mirror, gutters and rails swap sides, headings and cells right-align, modal
  close controls and footer button groups mirror correctly, and labels, tabs, titles, chips,
  placeholders and templates are translated. The residual findings are numeral-system consistency and
  time-string localisation — all S3 or S4.
- **The entitlement engine is a faithful, test-pinned port** of the mockup's own module: the same
  three cumulative tiers, the same feature keys, `TIER_RANK`, `minTierFor`, `exceedsBand` as an
  advisory that never hard-blocks, `suggestedTierForCount` — pinned by a unit test written against
  the demo's own cases. One near-miss was recorded specifically to prevent a false finding:
  `hasRole()` returns `true` for every feature, and **the mockup does the same, by design.**
- **The overnight business-day coordinate model reproduces the baseline's own worked example
  exactly** — `openTime 11:00` / `closeTime 02:00`, a booking at `2026-05-20T01:00` resolving to
  business day `2026-05-19` with the window `{startMin: 660, endMin: 1560}`. This is the same
  subsystem that carries BE-F03, and the distinction matters for remediation: the *forward*
  conversion and the day-window model are correct; only the inverse fails to roll the date, at two
  live call sites.
- **The transition FSM's default map is byte-equivalent to the baseline** — `scheduled →
  [in_progress, no_show, cancelled]`, `in_progress → [completed]`, the other three terminal, and
  `scheduled → completed` forbidden. The canonical map is right; two write paths do not consult it.
- **Tier gating shipped between the two audit phases and was verified working live during the
  audit.** When the admin phase ran, `shopCanAccess()` had no callers and no tier gate was reachable
  from the portal. `frontend/components/EntitlementFilter.php` has since shipped, attached at
  **application** level so a controller overriding `behaviors()` cannot bypass it. A Growth shop
  navigating to `/branch` — a `PRO_DELTA` feature — receives the plan-gate interstitial instead of the
  Branches screen, with the escape hatch to the plans page intact. **This is the clearest positive
  movement between the two phases and should be read as such.**
- **The fee formulas themselves are largely not what is wrong.** 42 contracts match the baseline
  exactly, including load-bearing ones: the processing fee's non-refundability, the marketing-fee
  formula and its minimum floor, the pending-until-locked rule, refund-zone resolution from the
  shop's cancellation policy, the attribution walk, five group invariants, collection-status
  derivation, the card-only tip model, the scheduling window and platform slot lock, and both
  notification arithmetic contracts. In several S1s the arithmetic is correct and the **call site is
  missing or the input is wrong** — a different and generally smaller class of work than rebuilding a
  fee engine.
- **Verification discipline held.** Ten candidate findings were refuted and dropped rather than
  quietly downgraded, including three where the *stated baseline was itself wrong* — the mockup does
  not partially complete a party when money is owed; the mockup's VAT-inclusive discount base
  produces exactly what the portal produces; and one finding did not survive its own arithmetic. One
  shop finding's evidence was rewritten because it contradicted another finding about the same money,
  and one FSM-bypass finding was examined for promotion to S1 and **deliberately held at S2** because
  the endpoint has no caller, no UI affordance and no cross-tenant reach.
- **Two seeded structural hypotheses were disconfirmed by measurement.** Page-level horizontal
  overflow was zero in all 32 admin screen × width probes, and every "overlap" the detector reported
  was SVG path geometry inside a single icon — no intersecting pair of cards, panels or text blocks
  was found on any screen at any width. Shop edit is a full page in the main document, not an iframe
  and not a modal.

---

## 10. Coverage and limitations

An honest boundary is worth more than implied completeness. What follows is what this audit does
**not** establish.

**Scope exclusions**

- **The `api/` tier is out of scope** per spec §2, along with the customer-facing mobile surface.
  Anything requiring a change to the `api/` data model or contracts is recorded as a
  contract-impacting item for sign-off, not scored as a defect.
- **Live Chat is on the exclusion list**; its presence or absence is not assessed anywhere.
- Everything on the 37-item demo-artifact exclusion list was subtracted before writing, in both
  phases.

**UI coverage**

- **Admin Track U reaches primary landing surfaces only.** All **38 modals, the 1 drawer, the 14
  panels** and every non-landing tab interior were **not opened** — the shop form, activation form,
  settlement workspace, balance-breakdown drawer, every editor and every `*-confirm` dialog. Only
  1440 was captured in that pass; only the super_admin / admin session was used, so permission-denied
  forks were not exercised; and most empty states were not reachable.
- **Shop Track U reaches 51 of 151 inventoried surfaces; 100 were not opened on both sides.** Group
  bookings end-to-end, calendar interaction states, booking-detail edge states, the Finance
  `Detailed charges` and `Invoices` tabs, Team Payroll and Structure, the Services extras tabs,
  Customers Classifications and Freeze-list, the Plans confirm and add-card modals, most Home
  dashboard panels, the whole shell layer and Help content. **No claim is made about any of them in
  either direction.**
- **Loading and error states are deliberately not graded** — the mockup reads a synchronous store,
  has no network layer, and renders none. There is no baseline. Validation and rejection states *are*
  graded.
- **`BOARD_SHOP.html` was not produced.** `BOARD_ADMIN.html` is on disk (4.13 MB, self-contained).
  Screenshots are in `02_EVIDENCE/admin/` and `02_EVIDENCE/shop/`; treat the directories rather than
  a count as the reference.

**Behavioural coverage**

- **65 live probes are recommended and were not run** — 30 admin, 35 shop across 31 contracts. Each
  carries a written recipe and a stated confirm/refute condition in its area file, and several state
  the condition that would **withdraw or narrow** the finding. Those withdrawal conditions are part of
  the finding and must not be dropped when a ticket is written.
- **The contract registers record `needs-probe = 0` and `unverifiable = 0` on both portals, which
  disagrees with those 65 probe recommendations.** Both reports follow the probe list as the more
  conservative reading: **no claim of complete behavioural coverage is made on either portal.**
  Reconciling the two is a re-run task, not a reporting one.
- **The admin S1/S2 probe run was interrupted mid-run** by a session limit. It had already captured
  10 screenshots naming candidate observations that were **not written up** — category parent
  handling, a city slug error, notification approval state. A re-run should reconcile those
  screenshots rather than assume they match the findings as filed.
- **`OQ-NOTIF-B` is unanswered** — whether a billable charge is appended on a notification path where
  no provider send occurs. If confirmed it is a further S1. **`OQ-FIT-A`** is resolved as latent, not
  active: `@@sql_mode` on the deployed database has not been checked, so whether a string written into
  an int column coerces silently or rolls back a transfer request is unknown; the risk becomes
  reachable the moment the first notification charge exists.
- Two shop probes are **blocked** rather than merely unrun (`C-SHP-007`, freebie minutes folded into
  the service period; `C-SHP-073`, the pay-now network-response half). One admin probe is blocked by
  tooling — `vendor/` is not installed in the audit checkout, so whether deleting a shop category
  attempts to delete every `Shop` pointing at it is **unresolved, not cleared.**
- Two shop verification areas (`booking-engine`, `shop-finance`) ran while the safety classifier was
  unavailable; [`RESUME.md`](RESUME.md) records that their output warrants a careful read.

**Staging state**

- **Commercial rates read zero across staging for most of the audit.** A probe raised them mid-way
  **and did not restore them.** Staging now applies real fees to any booking created there:

  | Field | Original | Current |
  |---|---|---|
  | `carry_forward_threshold` | 0.00 | 777.00 |
  | `marketing_fee_pct` | 0.00 | 8.00 |
  | `processing_fee_pct` | 0.00 | 2.50 |
  | `fixed_fee_sar` | 0.00 | 1.00 |
  | `sms_sell_price` | 0.0000 | 0.5000 |
  | `wa_sell_price` | 0.0000 | 0.7500 |

  Original values and the pre-probe ledger and transfer baselines are in
  `02_EVIDENCE/admin/test-data.md`. **This needs a decision on resume:** restore it if anyone else may
  test on staging, or keep it — the remaining fee probes need non-zero rates anyway. Note that **rows
  written before the change carry their stamped zero rate**, so old data cannot be used to validate
  anything.
- **Findings that will probe as 0.00 today, and must not be read as disconfirmed.** CF-CC-02 (both
  seeded offers carry `rate_discount_pct = 0`) · NOTIF-02 and F-FIN-06 (the same, from the allowance
  and fee sides) · every packages finding, including four S1s (**staging has zero service bundles**) ·
  BE-F03 (needs a shop configured `close_at <= open_at`) · SN-09 (the seeded triggers were
  force-approved by migration) · CF-CC-01 / BE-F04 (the endpoint has no caller). For NOTIF-02 in
  particular, **the wrong allowance is stamped regardless of the price**, so the defect is already
  present in the data; only the money appears later.
- **Other environment limits:** the shop's free trial is live with `trial_consumed = 1`, so the
  subscription money path could not be driven without an irreversible PAID ledger row (H6 is an
  inference from code plus three runtime probes, **not from a completed charge**); only one
  shop-owner session and one admin session were available, so permission-gated variants on both
  portals were unreached; and no portal credentials existed for a shop with no subscription row.

**Classification**

- **No confirmed finding on either portal was classified `drift`.** That is the recorded state, not a
  proof: `releases.ts` dating was available as a method but no per-finding release date is recorded,
  so "0 drift" means *no finding was identified as post-port mockup movement*, not *no post-port
  movement exists*. If a backlog item is contested on the grounds that the mockup moved after the
  port, `releases.ts` is where that is settled.

---

## 11. Re-run procedure

Per spec §15, with the exact commands.

**1 · Re-verify the mockup baseline.** A silently stale checkout invalidates the comparison; this
check exists because one previously cost a full re-port.

```
git -C "C:/Users/mmm_i/VS with Claude/Navagoo 2.0" fetch origin
git -C "C:/Users/mmm_i/VS with Claude/Navagoo 2.0" rev-list --count HEAD..origin/dev   # must be 0
git -C "C:/Users/mmm_i/VS with Claude/Navagoo 2.0" status --porcelain                  # must be empty
```

Update the pin in `02_EVIDENCE/environment.md` only if intentionally moving the baseline off
`cb48c8d`.

**2 · Start the mockup.** Vite serves the live working tree, so it represents the pinned ref only
while that tree is clean.

```
cd "C:/Users/mmm_i/VS with Claude/Navagoo 2.0/navagoo-app" && npm run dev
```

`http://localhost:6100/` — **not 5173**; `strictPort: true` means a second instance fails rather than
drifting to another port. Log in as **`ceo@navagoo.sa`** with any non-empty password. **It must be
super_admin** — `ADMIN_NAV` is permission-filtered, and auditing as a lesser role fabricates
missing-item findings. Routes are hash-based: `#/admin/home`, `#/shop/bookings`.

**3 · Capture the new staging build reference.** Replace the fingerprint table in
`02_EVIDENCE/environment.md`:

```
curl -sI https://stageshops.navagoo.com/css/tailwind.css
curl -sI https://stageshops.navagoo.com/css/custom.css
curl -sI https://stageadmin.navagoo.com/css/custom.css
curl -sI https://stageadmin.navagoo.com/css/adminlte.css
```

Record `Last-Modified`, `Content-Length` and `ETag`. Re-enumerate the commits between the build
timestamp and local `HEAD`, and re-state the divergence assessment.

**4 · Re-supply staging credentials.** They are deliberately never committed and do not survive a
session. Both portal URLs and the role used for each are in `02_EVIDENCE/environment.md`.

**5 · Configure the preconditions before any finance comparison.** `P-RATES` at minimum, plus
`P-OFFER`, `P-PKG`, `P-OVERNIGHT`, `P-WALKIN`, `P-SMSPRICE`, `P-PLANBUNDLE`, `P-NOSUB`, `P-NOTIFCHG`,
`P-CRON` and `P-MARKETING` for the items that name them. **Create fresh records afterwards** — rows
written earlier carry their stamped rate.

**6 · Re-run the retained orchestration script**, per portal, so either can be measured
independently against a later staging build:

```
Workflow({ scriptPath: "parity-audit/00_SPEC/workflow-area-audit.js", args: { … } })
```

**7 · Diff the new scorecard against this one** to measure closure — §3 here, and
`03_FINDINGS/admin/_PASS_RATES.md` / `03_FINDINGS/shop/_PASS_RATES.md` for the per-area figures. The
pass rate and the severity counts must be diffed separately; they do not move together.

**8 · Re-run Track D.** `03_FINDINGS/_stale-docs.md` and `03_FINDINGS/_open-items.md` cover the repo
as a whole and are written once, at the end. The 24 `open → done` entries should shrink as documents
are updated; any new `done → open` entry is the signal worth watching.

---

## Where everything is

| Artifact | Path |
|---|---|
| Admin verdict · backlog · evidence board | `REPORT_ADMIN.md` · `BACKLOG_ADMIN.md` · `BOARD_ADMIN.html` |
| Shop verdict · backlog | `REPORT_SHOP.md` · `BACKLOG_SHOP.md` |
| Per-portal findings summaries and S1/S2 indexes | `03_FINDINGS/admin/_SUMMARY.md` · `03_FINDINGS/shop/_SUMMARY.md` |
| Contract pass rates (Track L) and the denominator rule | `03_FINDINGS/{admin,shop}/_PASS_RATES.md` |
| UI parity (Track U) | `03_FINDINGS/{admin,shop}/_ui-parity.md` |
| Documentation currency — 34 entries | `03_FINDINGS/_stale-docs.md` |
| Consolidated open items — 151 rows, 57 needing a product decision | `03_FINDINGS/_open-items.md` |
| Baseline inventories and behavioural contracts | `01_BASELINE/{admin,shop}/` |
| Demo-artifact exclusion list — nothing on it may be reported as a gap | `01_BASELINE/demo-artifacts.md` |
| Environment, baselines, staging fingerprint, stated limits | `02_EVIDENCE/environment.md` |
| Live staging ledger and transfer-request observations | `02_EVIDENCE/admin/live-ledger-probe.md` |
| Staging records created during the audit | `02_EVIDENCE/{admin,shop}/test-data.md` |
| Governing spec — classification, severity, writing standard, re-run | `00_SPEC/PARITY_AUDIT_SPEC.md` |
| Resume point — what was and was not completed, and the traps | `RESUME.md` |
