# Subsale Database v2 (New Project) — the market data the project page reads

**Portal:** Manage · **Route:** `manage.property.catalog.subsale-v2.index` (`GET /manage/property/catalog/subsale-v2`) + `.show` (`GET …/subsale-v2/{id}`) · **Nav:** Portal → Analyze Property → **Subsale Database v2 (New Project)** pill (`PortalEngagementTabs`, between Subsale Database and New Project Database) · **Gated by:** the catalogue group's `admin` + `view-projects|view-subsale`; the override save (`PUT …/subsale-v2/{id}/override`, `.override.update`) also needs `manage-projects` · **The scrape is never written; overrides live beside it.**

## What it does

Lists the **EdgeProp source records** with a **Status** column and a per-record **override** (below) — `catalog_project_sources` rows of provider `edgeprop`,
15,520 on 2026-09-26 — and, per record, every asking rental and sale listing behind them. These are
the records the MY project page's analysis actually reads:

- **"Rental by Project (Nearby)"** on `/{country}/projects/{slug}` → Invest → Rental,
- the sale comparables and the **Sell / Rent** labels (which are listing COUNTS — 0 None, 1–5 Low,
  6–15 Medium, 16–30 High, >30 Very High — not a demand rating; `demandPresentation()` in
  `ProjectDetail/projectInvestment.js`).

Asked for by the owner on 2026-09-26 ("develop a new tab called Subsale Database v2 (New Project),
then transform the database that we use for new project into here") after it turned out that a number
on a customer page could not be traced to anything in the admin.

## ⚠️ The trap this page exists to expose

**The project page does NOT read the listings the Subsale Database edits.**

`ProjectDetailController::buildAnalysis()` runs the engine with `source_separated => true`.
In that mode `AnalysisEngineService::build()` takes the nearby projects from
`CatalogProjectSource::nearbyProviderCoordinates(DataProvider::CODE_EDGEPROP, …, 2.0)` and turns each
into an analysis project with `toAnalysisProject()`, whose `asking_rental_listings` /
`asking_sale_listings` come from the SOURCE row's `fields` JSON.

The Subsale Database's edit form ("Asking rental listings", `CatalogProjectForm.vue`) writes the
CANONICAL `catalog_projects.asking_rental_listings` — which that mode never reads. Editing there
changes nothing on the project page, silently. Other Analyze Property consumers that run without
`source_separated` DO read the canonical column, so the two can legitimately differ.

## How it works

- **`MarketSourcesController`** (`app/Http/Controllers/Manage/Property/`) — `index()` paginates the
  EdgeProp records (25 per page), `show($id)` renders one. Sorting runs on JSON expressions
  (`JSON_LENGTH` for the listing counts, `CAST(JSON_EXTRACT(…))` for sale PSF and transactions) with
  `catalog_project_sources.id` as the tiebreaker. The four header figures and the state counts are
  cached 10 minutes.
- **`MarketSourceQueryRequest`** — `search` (project name / area), `state` (multi), `listings`
  (rental / sale / both / none), `linked` (to a catalogue project or not).
- **`CatalogProjectSource::rentalSummary()`** — the PHP twin of the page's
  `buildPetaRentalComparables()`: rents outside **RM 200–50,000** ignored, median rent and median
  size **rounded to whole numbers** like the JS `median()`, rental PSF = median rent ÷ median size
  (NOT the median of each listing's own PSF). Verified 2026-09-26 on Vogue Suites 1 @ KL Eco City:
  the detail page's 2-bedroom row (13 listings, RM 3,800, RM 4.77) is exactly the VIIA Residences
  page's row. If the JS rule changes, change this method with it.
- **The detail page's "Rent by bedrooms" table is the one to compare with a customer page** — the
  project page filters to the unit's own bedroom count before it takes the median. The list's
  "Median rent" is all sizes together.
- **EdgeProp returns at most 50 listings of each kind per project**; the page says so.
- **The agent's phone is not sent** to the detail page (`listing()` drops it) — this page is for
  checking numbers, not calling anyone. Name and agency are shown.

## Status, force include and overrides (2026-09-26)

Built the same day, after ViiA Residences was missing from its own "Rental by Project (Nearby)"
(owner: *"i want the force include data and those function i mentioned"*).

**Why ViiA was missing — the worked example.** Its EdgeProp record passes every server rule (2 rental,
12 sale listings, 35 transactions, a high-rise, 2021). The page then keeps only the unit's bedroom
count within 1 km and runs `filterPetaRentalListings`' three two-sigma trims (size, rent, rent PSF).
ViiA had ONE listing per bedroom count, both at ~RM 6.2–6.3 psf against neighbours at ~RM 4.1–4.6 —
over the PSF cutoff (RM 5.43 for 2-bed, RM 5.96 for 1-bed) — so its only row went, and the project
with it. Not "too few listings": one listing far from the pack.

**Status column** (`MarketSourcesController::status()` / `STATUSES`): *Used* (passes the engine's
FIXED rules — evidence + comparable type), *Not used* (no listings and no transactions, or not a
comparable type), *Force included*, *Excluded*. The reason is the badge's tooltip. It cannot state the
PER-PAGE part (2 km of which project, the unit's bedrooms, the outlier trim), so *Used* means eligible.

**The override** — `market_source_overrides` (SITE table, `Src\Analysis\Reference\MarketSourceOverride`,
keyed by provider code + external id, one row per record, deleted when it changes nothing):
`inclusion` (Automatic / Force include / Excluded), `fields` (project name, area, completion year —
blank keeps EdgeProp's), `rental_listings` (a full replacement, or null for EdgeProp's), `note`.
Written by `MarketSourceOverrideRepository::save()` from the Show page's `MarketSourceOverrideModal`;
the list's pencil opens the Show page with `?edit=1`.

**Why a site table, not the master:** petav3 cannot write the master (`petav3_read`, and
`CATALOGUE_EDIT_DOMAINS` is `propertylabglobal.com`), and a sync rewrites `fields` anyway.
⚠️ Consequence: **overrides made on wk affect wk's pages only** — app.propertylab.com.my and any other
deployment keep EdgeProp's data. Moving them to the master (catalogue migration + the admin-domain
write gate) is the path if they must be shared.

**How it reaches the project page — `Src\Analysis\Services\MarketSourceOverlay`**, applied in
`ProjectDetailController::resolveAnalysis()` to EVERY payload on the way out (saved or fresh; what is
stored stays the engine's own). It must be read-time: the page serves SAVED analyses from the master's
`catalog_floor_plan_analytics`, mostly computed on another deployment, which this site cannot rewrite.
Per overridden record, matched by the scrape's project name:
- *Excluded* → its rental listings, rental comparables and `market_value` rows removed.
- *Force include* → its listing rows get `force_include: true`; `filterPetaRentalListings` keeps those
  past the sale-size check and the three two-sigma trims (bedroom count and radius still apply, and the
  forced rows do not move the others' mean/spread). A record the engine dropped entirely is ADDED from
  the scrape (or the admin's listings) when it lies within 2 km of the analysed project.
- `fields` → name / area / completion year rewritten on its rows; `rental_listings` → its rows replaced.

⚠️ **Not covered:** numbers the ENGINE derived server-side and saved — predicted PSF / market value,
and the Layout Analysis table (`layout_analyses`, built on the Hub). The rental figures members see are
computed in the browser from the listings, so they follow the overlay. Verified 2026-09-26 on ViiA's
real saved 1-bed payload (override inside a rolled-back transaction): ViiA appears first
(0 m · RM 4,000 · RM 6.29) and the other seven rows are unchanged.

**Own project first (2026-09-26).** A completed project is now priced on its OWN EdgeProp record —
market value on its asking PSF (≥ 2 own sale listings), rent on its own listings (≥ 1) — and its own
listings skip the outlier trim. `OwnProjectPricing` applies this at read time after
`MarketSourceOverlay` (an Excluded record never prices itself; a renamed one is matched by its new
name). Details: [project-detail readMe](/docs/modules_handbook/main/project-detail/readMe.md), Investment Analysis row.

To change a unit's headline rent regardless of comparables, the rent basis per layout on Live preview
still applies ([site-data-on-master.md](/docs/modules_handbook/shared/project-catalogue/site-data-on-master.md)).

## Related files

- [app/Http/Controllers/Manage/Property/MarketSourcesController.php](/app/Http/Controllers/Manage/Property/MarketSourcesController.php)
- [app/Http/Requests/Manage/Property/MarketSourceQueryRequest.php](/app/Http/Requests/Manage/Property/MarketSourceQueryRequest.php)
- [src/Analysis/Reference/CatalogProjectSource.php](/src/Analysis/Reference/CatalogProjectSource.php) — `rentalSummary()`, `fieldArray()`, `toAnalysisProject()`, `scopeNearbyProviderCoordinates()`
- [resources/js/Pages/Manage/Property/MarketSources/Index.vue](/resources/js/Pages/Manage/Property/MarketSources/Index.vue) · [Show.vue](/resources/js/Pages/Manage/Property/MarketSources/Show.vue)
- [resources/js/Components/Portal/PortalEngagementTabs.vue](/resources/js/Components/Portal/PortalEngagementTabs.vue) — the pill
- [src/Analysis/Services/AnalysisEngineService.php](/src/Analysis/Services/AnalysisEngineService.php) — `build()`'s source-separated branch, `collectRentalListings()`
- [resources/js/Components/ProjectDetail/projectInvestment.js](/resources/js/Components/ProjectDetail/projectInvestment.js) — `buildPetaRentalComparables()`, `demandPresentation()`
- [src/Analysis/Reference/MarketSourceOverride.php](/src/Analysis/Reference/MarketSourceOverride.php) · [src/Analysis/Repositories/MarketSourceOverrideRepository.php](/src/Analysis/Repositories/MarketSourceOverrideRepository.php) · [app/Http/Requests/Manage/Property/UpdateMarketSourceOverrideRequest.php](/app/Http/Requests/Manage/Property/UpdateMarketSourceOverrideRequest.php) · migration `2026_09_26_060000_create_market_source_overrides_table.php`
- [src/Analysis/Services/MarketSourceOverlay.php](/src/Analysis/Services/MarketSourceOverlay.php) — applied in `ProjectDetailController::resolveAnalysis()`
- [resources/js/Pages/Manage/Property/MarketSources/Partials/MarketSourceOverrideModal.vue](/resources/js/Pages/Manage/Property/MarketSources/Partials/MarketSourceOverrideModal.vue)
- [tests/Feature/Property/MarketSourcesTest.php](/tests/Feature/Property/MarketSourcesTest.php) · [MarketSourceOverlayTest.php](/tests/Feature/Property/MarketSourceOverlayTest.php) · `projectInvestment.test.js` ("force-included listings")
