# Jollibee Rewards — Operator Console prototype

A clickable **web app** prototype of the console Jollibee's marketing and loyalty team run the
programme from: the surface behind **[PRD-0007 Operator Surface](../../product/PRDs/0007-operator-surface.md)**'s
four sub-features and twelve stories.

> **Conformed to the RFI, 2026-09-21.** Where the RFI and the v29 SOW/business case disagree, the
> RFI wins — the owner's decision. Four things changed: the weighted-score fraud model became
> `PI-8`'s rule list; the Ops Assistant was removed; Settlement now carries **two versions side by
> side** pending a decision; the Franchise Partner role became **Store Scope**. See
> *[What changed and why](#what-changed-and-why)*.

> **Form factor:** desktop browser, not a phone. The member app prototype at
> [`../prototypes/member/`](../prototypes/member/) simulates a phone; this is a console — same tokens, same
> components, same brand, but the type scale tightens and the tables get dense, because PRD-0007 is
> explicit that here *"brand matters less than density and scannability."*

**Spec:** [UX-0007 Operator Console](../../ux/0007-operator-console/). Note that
[`agent-surface.md`](../../ux/0007-operator-console/agent-surface.md) there describes the assistant
this prototype **no longer has** — it is retained as the record of a removed surface, not as a spec
to build to.

---

## The one thing to understand first

**No button writes directly.** Every mutation goes through **one path**:

```
a button click  →  OC.propose()  →  Tools.preview()  →  the proposal card
                                          │
                                          ▼  (operator approves, with a reason)
                                   Tools.apply()  →  Store action  →  audit entry
```

There is no second write path. That is what makes the RBAC and audit claims in the spec **true
rather than narrated**: permission is re-checked on the action, scope filters the reads, and a
reason is required before anything is written.

PRD-0007 v2.2 makes preview-then-approve a **programme rule** across all 17 mutation paths, which is
why it survived the assistant that used to share it.

Try to publish a campaign you sent for approval and it refuses — not by hiding the button, but
because `Store.approveCampaign` checks the actor.

---

## View it

- **Just open the file:** double-click **`app.html`** (or `index.html` for the launcher). No build
  step, no npm, no key — nothing here calls out.
- **Serve it:** `./serve.sh` (or `python3 -m http.server 8080`) → `http://localhost:8080/app.html`.

The **dev harness** along the bottom carries a **role switch** — the only way to see that
out-of-scope subjects are *absent* from results rather than shown and refused.

Sign in starts as **Loyalty Ops**, deliberately **without fraud-tuning authority**, so the rule
escalation is demonstrable on first load rather than needing a role change to find.

---

## The thirteen views

| View | Stories | What it demonstrates |
|---|---|---|
| **Overview** | — | A **work queue, not a dashboard**. Held scans, rules with no threshold, placeholder economics — and an explicit note about why there are no charts |
| **Programme configuration** | 7.1.1, 7.1.2 | Two blocks, a table per category. **Settings** — the 16 that can be changed here. **Programme values** — the 15 set in the platform configuration. The write path refuses the second block, so the split is real. |
| **Reward catalog** | 7.1.3, `C-5` | Listings and point costs, withdrawal that leaves issued vouchers alone, the **A5** reward-cost arithmetic — and each reward expands to its **voucher-code stock**: batches in draw order, the codes themselves with a search over all of them, and per-code disable. Cost and supply are one question about one reward |
| **Campaigns** | `MK-1` | Bonus events and targeted offers. **Published under approval** — and whoever sent a campaign for approval cannot publish it, seeded so the gate is visible on first load. Audiences are **picked from Prism, never composed here** |
| **Members** | 7.2.1, 7.2.2, `PI-6` | Phone lookup, award / deduct with mandatory reason and bounds, no negative balance, revocation that spares already-redeemed vouchers — plus the **case**: held scans, linked accounts, vouchers and audit trail for one member, and the **freeze-then-suspend ladder** |
| **Stores** | 7.2.3, `ST-6` | Acceptance mode, and the **holdout flag treated as a measurement obligation**. Scoped to the user's own stores for a Store Scope role |
| **Fraud rules** | 7.3.1, 7.3.2, `PI-8` | **Six rules**, every threshold **unset**. Per rule: threshold, action on breach, scope, effective-from — the four `PI-8` names. Plus the authority gate that drafts rather than applies |
| **Held scans** | 7.3.3 | The review queue, carrying the **observations** a rule would read — counts and multiples, not scores. **This takes a side on open question 14** |
| **Audit log** | 7.4.3 | Actor / subject / change / time / reason, filterable by who. Append-only |
| **Access & roles** | 7.4.1, 7.4.2 | Permissions, scope, and the action no role may take on itself |
| **Settlement** | PRD-0006 6.4, `FR-5` | **Two versions, pending a decision.** v1 as built — store charging at a ₱0.10 `[placeholder]`, agree / pay / overdue. v2 — `FR-5`'s monthly NET settlement report and nothing else, which is what the RFI commits at pilot |
| **Reconciliation** | PRD-0006 6.4.3 | What the programme owes each partner, line by line, and whether they agree it |
| **Programme report** | PRD-0006 6.4.3 | **Measured** breakage against the catalog's assumption, and actual reward cost against the projection. Corporate scope only |

Plus the **review panel** on the right — where every change previews before it is applied. It is
not a page, and it opens only when there is something to approve.

**Reconciliation is the only view that is not PRD-0007's.** It realises
[PRD-0006 §6.4](../../product/PRDs/0006-redemption-crew-burn.md) here because that is where the
Store Scope role's stated surface — *"reconciliation reports"* — has to live. It also holds the
programme's **actuals**, which is the honest way to close the `A5` breakage gap the Reward catalog
otherwise fills with an undefended assumption.

---

## What is real, and what is not

**Real.**
- The **tool registry** — every button mutates through the same Store actions, and preview-then-
  approve with a mandatory reason runs on **all 17 mutation paths**.
- **Role permissions and scope filtering.** A Store Scope role sees 2 stores, 0 members, 0 held
  scans, and the views they cannot use are absent from the rail.
- The **audit log**, append-only, recording actor / role / action / subject / change / reason.
- The **authority gate** — Loyalty Ops may draft a rule change and not apply it (7.3.2).
- The **reward-cost arithmetic** (it reproduces PRD-0007's 86–90% breakage figure from the
  100-points-per-item benchmark, which is how we know the model is right).
- The **rule check** — a threshold counted against this console's own recent scans, for the two
  rules whose observable the fixtures carry. The other four say so rather than inventing a number.

**Mocked.**
- The fixture population — members, stores, catalog, held scans, seed audit history.
- The **held-scan observations.** [PRD-0003](../../product/PRDs/0003-anti-fraud.md) marks the
  detection modules `[not yet built]`, so they are seeded, not computed.
- Notifications. Anything Prism. The member app (this console does not write to it — see below).

**Absent by design.** Charts, KPIs, trends, segments — analysis belongs to Prism.

**Not on screen at all.** The console never cites the documents that specified it: no PRD numbers,
story numbers, `[confirm]` tags or open-question references. Those live in `screen-manifest.json`
and the specs. Every view and field carries operator-facing helper text instead, and the
verification driver fails the build if spec vocabulary reaches a screen — see
[CONSOLE-VOICE.md](CONSOLE-VOICE.md).

---

## Structure

```
prototypes/operator/
├── index.html            launcher
├── app.html              THE console — shell + thirteen view containers + the review panel
├── serve.sh              optional local server
├── screen-manifest.json  every view + what's real
├── AI-GUIDE.md           the authoritative build guide — read before editing
├── CONSOLE-VOICE.md      the operator register, and why VOICE.md INVERTS here
├── BUILD-LOG.md          per-wave log
├── css/  tokens.css · components.css   (verbatim from ../prototypes/member/)
│         console.css                   (the only net-new CSS — desktop shell, dense tables)
└── js/   icons.js        (from ../prototypes/member/, plus console icons)
        simulations.js    OPS — fixtures. PRD-0000's config table, PI-8's six fraud rules
        store.js          Store — the ledger, RBAC, scope, audit. THE ONLY WRITE PATH
        tools.js          Tools — the registry every button mutates through
        app.js            OC — shell, the thirteen views, the review panel, the proposal card
        dev-toolbar.js    the harness (fixed palette — the documented tokens exception)
```

---

## What changed and why

**RFI-first conformance pass, 2026-09-21.** The v29 SOW and business case sell more than the RFI
describes. Asked which boundary wins, the owner chose **the RFI**. Four consequences:

| Changed | Why |
|---|---|
| **Fraud tuning → Fraud rules** | `PI-8`: *"Rules-based today… Thresholds, the action taken on breach, and the scope a rule applies to are all configuration… versioned with an effective-from date."* The console implemented **five weighted module scores feeding banded outcomes**, which is not what a reader of `PI-8` takes "rules-based" to mean. Now six rules, four configurables each. The band replay went with the scores |
| **Ops Assistant removed** | It appears in **no Jollibee PRD** and in no RFI answer. v29 puts agents at **Phase 2** — a member concierge and campaign-generation agents — and an operator-console assistant is not among them. `js/agent.js` and the Netlify proxy are deleted. The propose → preview → approve flow it shared with the buttons **stays**: PRD-0007 v2.2 makes that a programme rule, not an assistant feature |
| **Settlement carries v1 and v2** | Three RFI answers say the v1 machinery is not at pilot — `SC-1`, `FR-5`, `FR-9` — and the ₱0.10 funding value has no RFI source. But `FR-5` **does** commit a monthly NET settlement report today. Both are on screen, switchable, so the difference can be seen before either is removed. **This is the open decision** |
| **Franchise Partner → Store Scope** | The scoped store view — access bounded to the user's own store — is committed, so the **mechanism stays**. **Corrected 2026-10-19:** the reason given for dropping the word *franchise* no longer holds. The pilot is **six or more branches including a franchise-owned one** and a second-city branch, so franchise scoping is real pilot scope. Named roles are also retired in the specification in favour of five independent permission areas; this build still models three named roles and has not been re-expressed. |

**Also dropped:** invoice-sequence observations on held scans. The old Sequence-anomaly module fed
them, and `PI-8`'s configurable set has no rule that reads invoice sequence — *"new rule types
beyond the configurable set require a release."* An observation no rule can read is not evidence an
operator can act on.

---

## Relationship to the member app

**Standalone by design, for now.** This console does not write to the member app's ledger — the two
prototypes hold separate `localStorage` stores. Wiring an award here to move the balance in
[`../prototypes/member/`](../prototypes/member/) — the API-backed member build, whose ledger already lives on
the JFC loyalty API — is a good demo and a real scope decision, deferred rather than forgotten.

---

## Provenance

- **Design system:** `nmblr.foundry-codex` tokens + the `fds-*` component mirror, under the official
  Jollibee palette — all reused **verbatim** from [`../prototypes/member/`](../prototypes/member/).
- **Spec:** [UX-0007](../../ux/0007-operator-console/), grounded in
  [PRD-0007](../../product/PRDs/0007-operator-surface.md) v2.0 and
  [PRD-0000](../../product/PRDs/0000-jollibee-rewards.md) v2.2.
- **Agentic operations** remain in [PRD-0007 §7.5](../../product/PRDs/0007-operator-surface.md#sub-feature-75-agentic-operations-future) tagged `[future]`, and the console no longer
  implements them. Removing the surface does not retire the sub-feature — it stops the prototype
  demonstrating something the RFI never offered.
