# 案例复盘 — Case Debrief (Main · User Portal)

**Portal:** Main · **Routes:** none of its own — it is a TAB of the Learning Hub
(`GET /property/academy`, `Main\Portal\CoursesController@index`), addressed as
`?tab=cases`, and a frame inside it as `?tab=cases&case=<key>&s=<screen>&p=<point>` ·
**Nav:** "Learning Hub" → **案例复盘** tab, third in the strip (after 「How to Use PropertyLab」
and 「DMAIC 之路」) ·
**Gated by:** the portal group (`['auth','main','contact.verified']`) — nothing inside is a gate ·
**Server props:** **none**. The tab is content + behaviour only.

> ✍️ **Before touching any copy here, read
> [content-rules.md](/docs/modules_handbook/main/dmaic-road/content-rules.md)** — the same
> first-time-reader rules the road obeys, because it is the same reader.
> `resources/js/utils/cases/cases.test.js` machine-checks five of them.

## What it does

Takes ONE finished property purchase apart, screen by screen, until the decision that produced
the outcome is visible — then hands the reader five questions to ask about the property they are
looking at tonight.

**The road and a debrief run in opposite directions, and that is why this is a tab of its own.**
DMAIC 之路 walks a purchase FORWARD: what to do next, a card at the end of every stop. A debrief
starts from a result that already exists — the price paid, the rent collected, the six years —
and works BACKWARD to the decision. A member who has just read what to do next is exactly the
person who should see what it costs to skip a step.

It is also NOT a station's 「真实案例」 beat: that is two paragraphs beside the method it
illustrates. A debrief is seven screens with its own arithmetic, its own diagrams and an ending
that asks the reader for answers.

### Case 01 · 布城边上那一排 landed

The founder's own purchase, told in the first person and still in his name. Bought at
**RM 580,000** beside Putrajaya (Dengkil) on beliefs none of which was ever checked (the
「有地 = 稀缺 = 升值，所以贴钱正常」 chain, and a High Speed Rail station planned 2–3 km away),
held **6 years** at **−RM 1,200 a month**, worth about what it cost.

**Source (2026-09-06).** The founder's two-hour Zoom telling of it on 2026-09-04 —
「买 Landed 一定会升值？一间房教我的 3 个代价」— transcript plus sixteen slides (rev2), kept as the
upload `/file/e2125fa5-aba2-40de-ba98-8633ebd391fe` (`GMT20260904-120048_Recording.transcript.zip`:
the Zoom `.vtt` and the `.pptx`). The case was rewritten from it: every figure is one he stated there or is derived
from those. What the telling added to the first draft: **why that plot at all** (he wanted a
RM 1M+ freehold township in Putrajaya/Cyberjaya, could not borrow it, passed Cyber South for
being leasehold, drove 3–4 minutes further for a freehold row — 「自我安慰」), the top-up being
**planned** rather than discovered, the **two months** a tenant change costs, **OST** (Own Stay
Test) and **BMS** (Bank · McDonald's · Starbucks) by name, the HSR's actual end (January 2021,
> RM 320m compensation; revival rumours since 2023, no decision), **Cyber South's transaction
report** as the township-next-door comparison (launched ~RM 520k, still RM 530–550k in 2025, but
letting at RM 1,400–1,600), the **brother's Seremban 2 → KLCC commute** (1.5–2.5 h, now by
motorbike — a township that fails OST), and the **35-year hold horizon** that closes it.

Eight screens: 当时的判断 · 我买到什么 · 每月的数字 · 等不到的车 · 六年后 · 三条教训 ·
再等 35 年 · 换成你 — 41 points, about 15 minutes.

Three rules come out of it, and the last screen turns them into six questions (OST is the
sixth, under rule 1):

1. **买今天成熟的地方** — 成熟看今天，不看承诺 — and you would live there yourself.
2. **看不见的规划不算数** — 能开车去看得到的才算; 「以后会」 is the most expensive phrase.
3. **landed 要买整体城镇规划** — 买整座城，不买一排 (Setia Alam is the counter-example; the
   brother's Seremban 2 is the township that still fails OST).

**Every figure is derived, never typed.** `DEAL` holds the numbers the owner actually stated
(buy price, instalment, rent, years, plus the few the talk added — the township he wanted, the
neighbour's launch/2025/rent figures, the tenant-change months, the 35-year horizon and its three
growth rates) and `NUMBERS` computes the rest — the RM 86,400 of six years' top-ups, the 2.7%
gross yield against a 4% loan, the 1.4%-a-year growth, the −RM 36,400 in cash, the RM 666,400
break-even sale price, the RM 5,000 a tenant change costs, and the 35-year horizon (RM 504,000 of
top-ups against ≈ RM 822k / 1,632k / 3,199k at 1 / 3 / 5% a year, rounded to the thousand). The
prose, the diagrams and the catalogue card all read the same constants, so they cannot drift;
`cases.test.js` re-does the arithmetic.

**Two people, two cards.** Wai Kit's card is on the first point; the brother's (`BROTHER_CARD`)
is on the ONE point that spends his numbers (第 6 屏 · 哥哥的芙蓉第二城), per content-rules.md 3 —
`cases.test.js` checks the card sits on his first mention.

⚠️ **RPGT is deliberately NOT charged against this case.** A citizen selling in year 6 pays 0%,
and the screen says so instead of implying a tax that would not be owed — the honest version of
the loss is what makes the rest believable (content-rules.md 11).

## How it works

**No server, no table, no route.** The tab renders from data files and keeps the reader's place
in `localStorage`. That is a deliberate stopping point, not an oversight: the road's copy lives in
JS for the same reason (it is versioned, reviewed and testable), and a Manage authoring surface is
worth building only when the number of cases makes editing a file the bottleneck. When
member-level "who read what" is wanted, it lands with the portal guide's — see *Adding a case*
below for the two functions that would change.

**Reading grammar is the road's, on purpose.** A case's point is the same object a station's is —
`{ key, title, line, calc?, viz?, terms?, character? }` — so `Components/Road/PointStepper.vue`,
`CalcSteps.vue`, `CharacterCard.vue`, `Term.vue` and `VizHost.vue` render it with nothing new to
learn. A member who has walked a station already knows how this moves.

- **`Term.vue` gained one additive prop, `entry`** (2026-09-04). A case passes `caseTerm(k)`,
  which reads `utils/cases/terms.js` first and the road's registry second. The case's own words
  **cannot** live in `utils/road/terms.js`: that file's test walks the ROAD and fails on any entry
  whose `definedAt` is not a unit on it — correctly, and a case is not on the road. Words the road
  already defines (`rpgt`, `net-equity`, `psf`) are reused through the fallback, so there is still
  one definition per word portal-wide.
- **The viz registry is now portal-wide, not the road's.**
  `Components/Road/vizRegistry.js` globs the cases' `Viz/` folder alongside the road's two. ONE
  registry, because one component (`VizHost`) resolves both — a second lookup table would let the
  same kebab-case kind mean two different drawings depending on which page mounted it. **Kinds are
  unique portal-wide.**
- **Addressing.** `?case=&s=&p=` is written with `history.replaceState`, the same mechanism
  `ShowTabs` uses for `?tab=` and for the same reason: a refresh and a shared link must land on the
  frame the reader was on. A point is not a history entry — Back leaves the tab.
- **Resume.** Opening a half-read case returns to where the reader stopped. A debrief is one
  argument; restarting it at screen 1 is how a reader abandons it the second time. A case they have
  finished opens at the beginning again (「再读一次」).
- **The five questions are answered in the component and nowhere else** — no server, no
  `localStorage`. They are about a property the portal knows nothing about, and a stale answer to
  「这个地方今天有人潮吗」 restored a month later would be worse than none. Three answers, never
  two: 「不确定」 is the state the case is ABOUT (the owner's own four 「不确定」 cost him six
  years), so a yes/no control would hide exactly what it exists to surface. The verdict is a
  SENTENCE that names the rules the answers broke — never a score, which would invite comparing
  two properties on a number this cannot support.

**The last screen hands off to tools that exist**: Area Guide (`?tab=area-guide`), Analyze
Property (`/analyze-property/new-projects`), Wealth Planning (`/wealth-planning`) and the road's
M station. `cases.test.js` asserts the hrefs stay inside those prefixes.

## Adding a case

1. Write `resources/js/utils/cases/<key>.js` — the shape is `putrajayaLanded.js`: derive every
   figure from the few the owner stated, one `screens[]` of `points[]`, `rules[]`, a `checklist`
   whose every question names one of those rules, `objectives` ×3 and `recap` ×3.
2. Add it to `CASES` in `resources/js/utils/cases/index.js`. Numbering (`no`) is written into the
   case, not derived — a case keeps its number when another is inserted before it.
3. Diagrams go in `Pages/Main/Portal/Lms/Partials/Cases/Viz/`. The registry globs the folder, so
   nothing registers them. A **cumulative** drawing (one that declares `const ORDER` and asks
   `reached()`) is checked by rule 18's test: the case may never walk it backwards.
4. New words go in `utils/cases/terms.js` — not the road's.
5. Run `npx vitest run resources/js/utils/cases resources/js/Pages/Main/Portal/Lms/Partials/Cases`.
   Then **read the case cold, as a member**: whether a paragraph teaches, and whether the Chinese
   reads as Chinese, is the half no test sees.

*(If a case ever needs server-side progress, it is `CasesTab.vue`'s `load()` / `save()` and
nothing else — they are the only two places that touch storage.)*

## Related files

**Frontend — content (pure data, no Vue)**
- `resources/js/utils/cases/index.js` — the registry (`CASES`), the reading walk (`casePoints`),
  `pointText`, `pointCount`.
- `resources/js/utils/cases/putrajayaLanded.js` — case 01: `DEAL`, `NUMBERS`, `WAI_KIT_CARD`,
  seven screens, `RULES`, `CHECKLIST`, `TOOLS`.
- `resources/js/utils/cases/terms.js` — the case term registry, `caseTerm()` (road fallback),
  `caseMentions()`.

**Frontend — components**
- `resources/js/Pages/Main/Portal/Lms/Partials/Cases/CasesTab.vue` — catalogue, local progress,
  the `?case=&s=&p=` URL.
- `…/Cases/CaseReader.vue` — the screen rail, the stepper, and everything in its `#below` slot
  (objectives · character card · term chips · checklist · tools · recap).
- `…/Cases/CaseChecklist.vue` — the five questions and the verdict.
- `…/Cases/Viz/` — `CaseSiteMap` · `AssumptionCards` · `PocketLand` · `MonthlyGap` ·
  `DemandChain` · `CaseScoreboard` · `CaseRules`. (`demand-driver-icons` is the road's, reused on
  screen 4 — the same five demand sources, so the case and the method name them identically.)

**Frontend — shared, touched by this module**
- `resources/js/Pages/Main/Portal/Lms/Index.vue` — the tab entry and its slot.
- `resources/js/Components/Road/vizRegistry.js` — globs the cases' `Viz/` folder.
- `resources/js/Components/Road/Term.vue` — the additive `entry` prop.

**Tests**
- `resources/js/utils/cases/cases.test.js` — shape, rule 1 (first use), rule 16 (spelled out),
  rule 17 (no chained arithmetic), rule 18 (the picture never runs ahead), rules 14 · 15 (neither
  word appears in any case source), and the arithmetic of case 01.
- `resources/js/Pages/Main/Portal/Lms/Partials/Cases/CasesTab.test.js` — resume, walking past a
  screen boundary, the URL, and the three verdicts.
- `…/Cases/Viz/casesViz.test.js` — every state the case names renders, with an aria-label and a
  caption, and the money diagrams quote the case's constants.

**Backend** — none. **Migrations / Seeders / Routes** — none.

## Related modules

- [DMAIC 之路](/docs/modules_handbook/main/dmaic-road/readMe.md) — the method this case is the
  reverse of, and the owner of the reading grammar (`PointStepper`, `VizHost`, `Term`) and the
  [content rules](/docs/modules_handbook/main/dmaic-road/content-rules.md).
- [How to Use PropertyLab](/docs/modules_handbook/main/portal-guide/readMe.md) — the other
  Learning Hub tab built on the same stepper, and the model for the `localStorage` progress this
  one copies.
- [Area Guide](/docs/modules_handbook/main/area-guide/readMe.md) — where screen 7 sends the reader
  to answer 「这个地方今天有没有人潮」.
