# Mobile impact — parity-audit remediation changes (2026-08-11/12)

**To:** Mobile team (customer app + specialist/agent app)
**From:** Backend — parity-audit remediation program (`tailwind-poc`, commits `727b88d` → `bdf9e2f`)
**Scope:** every change in this program that an app can observe. Everything else in the
program was portal/admin/common-ledger work with **zero** mobile surface.

## The one guarantee to read first

**No request or response SHAPE changed anywhere.** No field was added, renamed, removed
or re-typed; the error envelope (`{success:false, status, message, errors}`) is untouched.
Every item below is a **value or behaviour** change. Items are classified:

| Tag | Meaning |
|---|---|
| 🔴 ACTION | The app should verify/adjust a screen or flow |
| 🟡 VERIFY | Behaviour changed in a way QA should re-test; code changes likely not needed |
| 🟢 FYI | Server-side only; no app work |

Summary:

| # | Item | Endpoint(s) | App | Tag |
|---|---|---|---|---|
| 1 | Redemption booking valued from the OWNED snapshot | `POST /subscription-package/redeem` | Customer | 🔴 |
| 2 | Refund zone now anchored at the SLOT time, not midnight | `POST /booking/cancel` (+ any cancel-preview UI) | Customer | 🔴 |
| 3 | In-app notifications fire while paid templates await approval | push/in-app feed | Both | 🟡 |
| 4 | Redemption visits: no earnings minted on completion (agent path) | `agent/bookings` complete | Agent | 🟡 |
| 5 | Walk-in Payment rows start On-Hold until completion | agent booking/payment views | Agent | 🟡 |
| 6 | Classification written at booking CREATION + mobile-number fallback | `/booking/*`, `/subscription-package/*`, `/group-booking/create` | Customer | 🟢 |
| 7 | Package-sale marketing fee waived inside the grace window | `POST /subscription-package/purchase` | Customer | 🟢 |
| 8 | Group cancels: **NOT affected** (correction of an earlier note) | `/group-booking/cancel-*` | Customer | 🟢 |
| 9 | Subscription charges stamped `charge_to_card` | (shop SaaS, not the customer app) | — | 🟢 |
| 10 | CORS allow-list restored on shop read endpoints | `/shops/*` | — | 🟢 |
| 11 | Social endpoints now actually configured server-side | `/social/*` (shop-portal feature) | — | 🟢 |

---

## 1 · 🔴 Redemption booking is valued from the owned snapshot (PKG-08 · `1d7267a`)

**What changed (server).** `POST /subscription-package/redeem` used to build the visit
booking from the **live catalogue price** (`shop_service.service_amount`) at redemption
time. It now uses the entitlement's snapshotted **`per_session_price`** — what the
customer actually pre-paid per session. Entitlements created before the snapshot column
(NULL) keep the old catalogue basis, so historic behaviour is preserved for them.

**Call flow (after):**
```
App                                   Server
 |  POST /subscription-package/redeem  |
 |------------------------------------>|  ent = load entitlement (FOR UPDATE)
 |                                      |  gross = ent.per_session_price ?? live catalogue   <-- CHANGED
 |                                      |  booking.sub/vat/total derived from gross
 |                                      |  sessions_remaining--
 |<-- 200 {data: {booking..., total_amount, per_session_price, ...}}
```

**How it affects the app.** For NEW redemptions, `sub_amount` / `vat` / `total_amount`
on the returned booking (and on that booking everywhere it is later fetched —
`/booking/index`, booking details) will normally be **lower** than before (packages are
discounted vs the catalogue), and will **no longer move** if the shop edits the service
price afterwards. The redeem response already exposes `per_session_price`, so the two
figures now agree.

**What mobile should do.**
- Re-test every screen that shows a redemption visit's amounts (redeem success screen,
  booking details, history rows): confirm nothing hard-codes an expectation that the
  visit value equals the live service price.
- If any screen cross-checks `booking.total_amount == shopService.price`, drop that
  check for `payment_mode == "package"` bookings.
- Nothing to change in requests.

---

## 2 · 🔴 Refund zone anchored at the appointment's slot time (FIN-LEDGER-03 · `9d0b5f4`)

**What changed (server).** The refund-zone anchor used to resolve to **midnight** of the
appointment day (the time-of-day was discarded). It now resolves to
`booking_date + from_hour`. The zone (`full / partial / none`) drives both the marketing
reversal and — on the customer cancel path — **the refund amount returned to the app**
(`api/BookingController` reads `refundZoneFor` directly).

**Call flow (after):**
```
App                                   Server
 |  POST /booking/cancel {id}          |
 |------------------------------------>|  anchor = strtotime(booking_date + ' ' + from_hour)   <-- CHANGED
 |                                      |  hoursBefore = (anchor - now)/3600
 |                                      |  zone = hoursBefore >= full ? FULL : >= partial ? PARTIAL : NONE
 |                                      |  refund computed from zone; reversal per zone
 |<-- 200 {data: {..., refund fields, check_refund_type}}
```

**How it affects the app.** Refund outcomes SHIFT (correctly) by up to 24h near the
thresholds. Worked example (shop window full ≥ 48h / partial ≥ 24h): cancelling **30h
before a 20:00 appointment** used to be FULL (midnight anchor made it look like ~44h);
it is now **PARTIAL** — which is the intended product rule.

**What mobile should do.**
- QA the cancel flow near both thresholds (just above/below `refund_period_start` and
  `refund_period_end`) and any copy that PREDICTS the refund before confirming.
- If the app computes an expected zone locally for display, make sure its local math
  also uses date+time (not date alone), or rely solely on the server's response.
- `check_refund_type` / refund fields keep their names and types.

---

## 3 · 🟡 In-app notifications fire while paid templates await approval (NOTIF-01/03 · `a317453`)

**What changed (server).** Channel approval is now per-channel, and the **in-app channel
never requires approval** (it is platform-authored). Previously a trigger whose SMS/
WhatsApp copy was pending approval was silenced ENTIRELY — including its free in-app
message.

**How it affects the app.** Both apps will start receiving in-app notification rows (and
their pushes, where wired) for triggers that were previously silent while a paid template
sat pending. **Volume can increase.** No payload change — same `notifications` rows,
same `data.type` conventions.

**What mobile should do.** Nothing code-wise. Expect more in-app items during QA; if any
dashboard counts "unseen", it simply counts more.

---

## 4 · 🟡 Agent app: completing a redemption mints no earnings (PKG-03 · `bdf9e2f`)

**What changed (server).** When a specialist completes a **package-redemption** visit
from the agent app, the server no longer creates Earnings/ShopEarning rows from the
booking total (the visit collected 0.00 — the package SALE was the money event). All
other completions are unchanged.

**Call flow (after):**
```
Agent app                              Server
 |  POST agent/bookings (complete)      |
 |------------------------------------->|  payment.status = paid
 |                                       |  if (!booking.isPackageRedemption)      <-- CHANGED
 |                                       |      createBookingEarnings(...)
 |<-- 200 (same response shape)
```

**What mobile should do.** If the agent app's wallet/earnings screens listed an earnings
row per completed redemption, those rows will no longer appear for NEW redemptions —
this is correct (specialist commission for redemptions is a separate wages-engine
question, not booking earnings). Re-test the earnings list with a redemption in the mix.

---

## 5 · 🟡 Agent app: shop-portal walk-ins carry `earning_status = On-Hold` until completed (BE-F07 · `ae7dec6`)

**What changed (server).** A walk-in created from the SHOP PORTAL used to stamp its
Payment row `earned_full` at creation. It now starts **On-Hold (0)** and is promoted to
`earned_full (1)` at completion; cancel/no-show keep it On-Hold.

**What mobile should do.** Only relevant if the agent app renders the payment's
`earning_status` label for portal-created walk-ins: a still-scheduled walk-in will read
"On-Hold" instead of "Earned (Full)" — correct, but visible. Agent-app-created bookings
are unaffected (their own path still stamps earned_full at completion time).

---

## 6 · 🟢 Classification written at booking creation + mobile-number fallback (CLS-02/CLS-03 · `6ff6825`, `1d7267a`)

- Creating a booking via `BookingForm` (customer app solo bookings) now writes the
  `customer_classification` row **at creation** (persist-once, soft-guarded — a
  classification hiccup can never fail the booking).
- The fee-gating classification reads used by `/subscription-package/purchase`,
  `/subscription-package/redeem` and `/group-booking/create` now fall back to the
  **normalized mobile number** when the customer re-registered under a new account id.

**Impact:** none on any response. This changes WHICH shops get charged marketing fees —
a shop-billing correctness fix. No app action.

---

## 7 · 🟢 Package-sale marketing fee waived inside the shop's grace window (PKG-07 · `6ff6825`)

`POST /subscription-package/purchase` still returns exactly what it returned. The fee
rows stamped against the SHOP (not the customer) are now grace-aware. No app action.

---

## 8 · 🟢 Correction: group cancels are NOT affected by the FSM change (GRP-04 · `5a692b2`)

An earlier commit note flagged a possible behaviour change on
`/group-booking/cancel-group` / `cancel-participant`. On inspection, the mobile
controller implements its own **customer-semantics** cancel and does **not** call the
portal service methods that were FSM-gated. Verified: no call sites. **Mobile group
cancel behaviour is byte-identical.** (The portal's Cancel-all/Complete-all are what
changed.)

---

## 9 · 🟢 Subscription charges ride `charge_to_card` (FIN-LEDGER-05 · `9d0b5f4`)

Affects the SHOP's SaaS billing ledger only (Navagoo Plans / renewal cron). The customer
and agent apps never see these rows. No action.

---

## 10 · 🟢 CORS allow-list restored on `/shops/*` (`9a69004`)

Response-header-only fix (the endpoint had regressed to `Origin: *`). **Native apps send
no Origin header and are completely unaffected.** Only browser-based clients are subject
to the `ALLOWED_ORIGINS` allow-list — if you ever test from a web origin, ask backend to
allow-list it.

---

## 11 · 🟢 Social endpoints configured server-side (`727b88d`)

`Yii::$app->uploadPost` / `socialMedia` are now registered (they previously threw
"Unknown component ID") and the status poller is scheduled. `/social/*` is a
**shop-portal** feature; listed only for completeness.

---

## Regression-test shortlist for the next app build

1. Redeem a session → check amounts on the success screen + booking details (item 1).
2. Cancel bookings at ~T-49h, ~T-47h, ~T-25h, ~T-23h against a 48/24 window → zones
   and refunds match the slot-time anchor (item 2).
3. Complete a redemption from the agent app → completion succeeds, no earnings row
   (item 4); complete a normal booking → earnings as before.
4. Sanity: `/booking/index`, group create→pay→confirm, package purchase — unchanged
   shapes (guarantee section).

Questions → same channel as the group-booking delivery notes
(`GROUP_BOOKING_DELIVERY_TO_MOBILE.md`).
