# 会籍 — Membership Plans, the pricing page (Main · User Portal)

**Portal:** Main · **Route:** `GET /membership/plans` (`main.portal.membership.plans`) →
`Main\Portal\MembershipPlansController@index` → `Pages/Main/Portal/Membership/Plans.vue` ·
**Nav:** none — reached from the webinar link, the dashboard CTAs and Profile → Membership; a
signed-in member also gets a `← Dashboard` link (GUIDELINES §15: a page with no sidebar entry owes
the reader a way back) · **Gated by:** NOTHING — it is **PUBLIC** (`public.site` middleware, beside
`/home`), because the person it is for has just watched a webinar and has no account. **Dual
chrome:** a guest gets `SiteLayout` and buttons that go to login (the controller sets
`url.intended` so they land back here signed in); a member gets `AppLayout` (`headerless`, sr-only
title — bound only under the portal chrome, SiteLayout has neither) ·
**Buys through:** the existing `POST memberships/{uuid}/checkout` (`CheckoutController`) — this
page never writes.

> ✍️ The copy is member-facing learning copy and obeys
> [content-rules.md](/docs/modules_handbook/main/dmaic-road/content-rules.md). Words the founder
> retired on 2026-09-05, all of which read wrong to a Malaysian Chinese first-timer or to an
> agency: **订房** (a hotel room), **每间房** (a room), **抵扣 / 抵回来** (not understood),
> **rebate / 回扣** (an agency reads commission handed back), **Clinic** (collided twice). The
> benefit is a **购房 voucher**, spent as booking fee. Every service is plain words first with the
> English name in brackets — 「一对一配盘 Zoom（Property Match）」.

## What it does

Sells the four membership tiers as four KINDS OF PERSON — Free 起步 · Plus 看房 · Growth 副业 ·
Pro 收租 — and states what becoming a **Property Client** gets you. Positioning is the founder's:
PropertyLab is an AI / decision / identity enabler, not a property-selling channel. The hero says
that; the voucher is the sub-line and names the channel it applies to (跟合作的中介 Agency Partner
买 PropertyLab 筛选的房产).

Top to bottom: hero → persona picker (lifts one card) → the four cards → the three guarantees under
their own columns → **Pro in one band** (you are not buying a property, you are joining a community
with lifetime support and learning) → **why be our Property Client** (five stations of one
purchase + the three questions a buyer asks) → the **实际成本 switch** (rolls every price to what
the voucher leaves: Plus RM 0 · Growth RM 0 · Pro −RM 2,612) → the buy rule + T&C → the comparison
table (collapsible groups, differences-only) → twelve service notes (anchors) → the RM 86,400 proof
→ FAQ.

**What is deliberately NOT on it:** countdowns, struck-through prices, "most popular" badges, logo
walls. The only scarcity is real capacity (`本月剩 N / 20 位`) from the subscriptions table.

## How it works

**Numbers vs words.** Every figure is a prop; every sentence is data in JS.

- `config/membership_plans.php` — per plan: `membership_code` (the `memberships.code` checkout
  sells; **null = no row yet**), `price` (the row's own price wins once linked), `credits`
  (per month × months — lifetime ACCESS, time-boxed credit), `voucher`, `capacity` (monthly
  limit on the human-served tiers, null = unlimited). Today: Pro → `elite` (RM 2,388), Growth →
  **`ai` — the existing AI Active Blueprint row (id 3, RM 299, untouched)**: its 156 members simply ARE Growth, the
  row keeps its name and price, the page prints "Growth" from config and RM 299 from the row. Plus → `plus` (RM 99,
  seeded by migration `2026_09_09_100001_seed_plus_and_growth_memberships`, copy modelled on the `pro` blueprint
  row). The ladder a member sees is RM 0 · 99 · 299 · 2,388.
  Growth is NOT `ai-add-on-member` (the Vibe Coding Implementation Day): the founder's 2026-09-09
  call is that the day is an EXTRA sold on its own, so the Growth copy never lists it and the
  guarantee says 参加后, never 来了那一天. What each tier can OPEN in the system is configured on Manage → Sales →
  Memberships → **Function access** (the `feature_gates` matrix, 2026-09-09) — the rows exist, the ticks are the founder's.
- `MembershipPlansController@index` merges config with the live rows, counts this month's ACTIVE
  subscriptions per membership (`capacity.used`), finds the highest plan the member holds
  (`current`, so an upgrade charges the difference), and reads `has_card` off
  `RoadPresenter::hub()` (a member with a saved road card is past "先学" — the hero says so).
  Reads only.
- `resources/js/utils/membership/plans.js` — the copy: `PLANS` (who / outcome / bullets as
  SEGMENTS so a term can link to its service note without v-html / guarantee), `PERSONAS`,
  `PRO_SUMMARY`, `CLIENT_JOURNEY`, `BUY_RULE`, `MATRIX` (rows marked `live` read the props),
  `SERVICES`, `PROOF`, `FAQ`, `CREDIT_RATES` (calibration placeholders — read the real chat /
  report ratio off the Hub's price list before launch).
- `resources/js/utils/membership/effectivePrice.js` — the one calculation: `listPrice()` (the
  difference on an upgrade, null for the plan held or below), `effectivePrice()` (what the voucher
  leaves; Pro's negative figure is the offer's argument), `hookLine()`, `creditLine()`. Pinned by
  `effectivePrice.test.js` the way the road pins math.js: 0 / 0 / −2,612 are never typed into a
  template.

**Interaction.** `who` lifts one card and dims the rest; `buying` (the switch) feeds every
`PlanCard`, whose price rolls to the new figure (rAF, 420 ms, off under reduced motion). The
credit number on each card is a popover (chats / reports). Underlined items are `#svc-*` anchors
into `ServiceNotes`, which highlights the `:target`.

**Motion.** `composables/useReveal.js` exports a LOCAL `v-reveal` directive (IntersectionObserver,
once, 12 % threshold): the element sits in its resting state until the directive proves it can
observe, so a print, an old browser or a reduced-motion reader gets the page fully visible and
still. Page-scoped CSS in `Plans.vue` (`mp-` prefix): `mp-rise` for the hero's staggered lines,
`mp-rv` / `mp-in` for scroll reveal, the journey rail that draws itself, a faint grid + one blue
glow (`animate-float` from app.css) behind the hero. Brand is white · navy-950 · brand blue; Pro's
rank is carried by its dark material, not a badge.

**Checkout.** A card's button `router.post()`s `checkout_href` (the existing hosted-checkout
flow); a plan with no sellable row or a full month renders a disabled button, never a dead link.
Free's button is a `Link` to the Academy.

**Every CTA is the same single-line pill, and the seat count sits BELOW it (2026-09-08).** The
count used to be a second row inside the button, so the two cards that have one (Growth, Pro) ran
~18px taller than the two that don't — and read in English, where `本期剩 30 / 30 位` becomes
"This intake — 30 of 30 left" against ~168px of label width, that line wraps and the gap doubles
to ~34px. Three rules keep the four buttons identical in any language: the count is its own row
(`[data-capacity]`), that row is **reserved on every card** — a non-breaking space where there is
no count — so the counts that do exist still line up across the ladder; all three button variants
(guest `Link`, Free's Academy `Link`, the checkout `button`) carry the same padding AND a border,
transparent on the filled ones, so the outlined Free button is not 2px taller; and every variant
is `items-center`, never `flex-col`. `[data-cta]` marks the button on all three.

## Before this can be announced

1. ~~Create the Plus row~~ — DONE 2026-09-09, migration `2026_09_09_100001` (code `plus`, RM 99).
2. ~~Confirm Growth = Vibe Coding Implementation Day~~ — DECIDED 2026-09-09: Growth IS the AI Active
   Blueprint row (`ai`, RM 299, nothing changed on it); the Implementation Day stays `ai-add-on-member`, sold as an
   extra.
3. **Retire the two RM 299 blueprints as tiers** (they become courses inside Plus / Growth) — or
   the Profile → Membership tab will list tiers this page does not.
4. **Calibrate `CREDIT_RATES`** from `ai_gateway_model_prices`.
5. **Credit expiry + top-up** must exist in the product before "24 个月配额" is true.
6. **Confirm the T&C sentences** — "价钱跟发展商一样，佣金发展商付" (the client-journey answer) and
   "voucher 当订购金用，不兑换现金" are the founder's to sign off; both are the sentences a reader
   will hold the company to.
7. **The auto-upgrade rule** (any member who buys through the Partner becomes Pro for life, without
   the RM 5,000 voucher) needs a write in the booking flow — it is stated on the page and not yet
   enforced anywhere.
8. Unlock `ANALYZE_PROPERTY_LOCKED` / `AREA_GUIDE_LOCKED` or the page sells screens members
   cannot open.

## Related files

**Backend**
- `app/Http/Controllers/Main/Portal/MembershipPlansController.php`
- `config/membership_plans.php`
- `routes/main.php` — `GET membership/plans`, PUBLIC, declared beside `/home` with
  `public.site`. A literal two-segment path, so neither the portal group's `membership` redirect
  (an exact match) nor the trailing `{slug}` funnel catch-all (one segment) can claim it. **Route
  cache:** `php artisan route:cache` after touching it — this box caches routes.

**Frontend**
- `resources/js/Pages/Main/Portal/Membership/Plans.vue` — the page + its `mp-` styles.
- `…/Membership/Partials/PlanCard.vue` · `ProSummary.vue` · `ClientJourney.vue` ·
  `CompareTable.vue` · `ServiceNotes.vue` · `PlanFaq.vue`
- `resources/js/utils/membership/plans.js` (copy) · `effectivePrice.js` (maths)
- `resources/js/composables/useReveal.js` (`v-reveal`)

**Tests**
- `resources/js/utils/membership/effectivePrice.test.js` — the three quoted figures, upgrade
  differences, the voucher on a difference, wording never says rebate.
- `resources/js/Pages/Main/Portal/Membership/Plans.test.js` — ladder order + prop prices, real
  capacity under the button, every CTA a single-line pill with the count outside it, 即将开放 for
  an unlinked plan, the switch (0 / 0 / −2,612), a Plus member's 你在这里 + differences, the
  persona picker.

**Design references (session artifacts, 2026-09-05)** — the offer review and the clickable
mockup this page was built from: "PropertyLab Offer v2" and "PropertyLab 会籍" in the founder's
artifact gallery.

## Related modules

- [Membership (Manage)](/docs/modules_handbook/manage/membership/memberships/readMe.md) — the
  rows this page sells; create / activate tiers there.
- [Payments → Payment Items](/docs/modules_handbook/manage/payments/payment-links/readMe.md) and
  `CheckoutController` — the hosted checkout the buttons post to.
- [DMAIC 之路](/docs/modules_handbook/main/dmaic-road/readMe.md) — `has_card` and the content
  rules; [案例复盘](/docs/modules_handbook/main/case-debrief/readMe.md) — the RM 86,400 proof.
