Skip to content

feat(costs): consolidated Costs section on listing detail - #11

Merged
timothygithinji merged 1 commit into
mainfrom
feat/costs-section
May 28, 2026
Merged

feat(costs): consolidated Costs section on listing detail#11
timothygithinji merged 1 commit into
mainfrom
feat/costs-section

Conversation

@timothygithinji

@timothygithinji timothygithinji commented May 28, 2026

Copy link
Copy Markdown
Owner

Closes item 6 of the improvement series. Last one.

Problem

The listing detail page scattered the actual cost of renting a property across three places:

  • Price card showed monthly rent
  • Fineprint listed deposit / fees / service charge / ground rent
  • Council-tax table sat lower down

To compare two listings you had to mentally aggregate all of these. The hidden answer to "what does this actually cost per month?" wasn't anywhere on the page.

Fix

New <Costs> (mobile) / <CostsCard> (desktop) — one ordered section with a "true monthly" total beneath. Both share a single computeCosts() function; the rendered HTML is identical except for the outer wrapper.

What you'll pay
─────────────────────────────────────────────────
Rent                                       £1,900
Council tax     Barnet · Band D              £190
Service charge  £1,200/yr                    £100
Deposit         £3,200 over 12 months        £267
Bills           Included                       —
─────────────────────────────────────────────────
TRUE MONTHLY                              £2,457

What's in the total

Item Math
Rent priceMonthly
Council tax councilTax.bands[listingBand].monthlyPounds — only when the band is known
Service charge serviceChargeAnnual / 12
Ground rent groundRentAnnual / 12
Deposit deposit / minimumTermMonths — amortised

Skipping council tax when the band is unknown (vs falling back to Band D) is deliberate — £1,500/yr properties shouldn't get silently rounded up to £2,200/yr ones.

What's NOT in the total (deliberately)

  • Bills. Shown as an indicator ("Included" / "Extra") but no monthly figure added. Bill amounts depend on usage and bedrooms in ways this layer can't model honestly; surfacing a fake number would make two listings look comparable when they aren't. When bills are excluded the total is flagged "True monthly (partial)".
  • Tenancy fees text. Illegal under the Tenant Fees Act 2019; Fineprint still shows the disclosure.

Where it mounts

  • Mobile: /listings/$clusterId between the portal cross-list and the highlights — costs land where the renter is thinking about money.
  • Desktop: InfoColumn between <PriceCard> and <AiCard>, wrapped in the bordered-card chrome to match <RecordsCard>.

Fineprint is unchanged — the full council-tax table + agent contact still surface there. Costs is the headline; Fineprint is the detail backup.

Verification

  • bun run typecheck
  • bun run lint:ci
  • bun run test298 / 298 pass (+11 new tests in tests/components/costs.test.tsx)

Tests use renderToStaticMarkup to drive the component end-to-end and assert on the rendered HTML — covers rent-only, rent + each cost type, deposit amortisation, deposit-without-term informational path, bills-included vs not, authority-known-band-unknown partial total, and an all-fields-present total of £2,507.

.tsx test files needed a one-line vitest config addition to the include glob — there's a comment in vitest.config.ts for future presentational tests.

Final state of the series

All six items now have open PRs:

Summary by CodeRabbit

  • New Features

    • Listing detail pages now display a consolidated costs breakdown showing rent, council tax, service charge, ground rent, and deposit with a calculated true monthly total.
  • Tests

    • Added comprehensive test coverage for the new costs component.

Review Change Stack

The listing detail page scattered the actual cost of renting a
property across three places: the price card showed monthly rent,
the fineprint listed deposit / fees / service charge / ground rent,
and the council-tax table sat lower down. To compare two listings
you had to mentally aggregate all of these.

This PR flattens them into one ordered section with a 'true monthly'
total beneath, so the decision-relevant number is visible at a glance.

## What's in the total

  - Rent (monthly).
  - Council tax (monthly amount for the listing's specific band; if
    the band is unknown, the row renders informational and the total
    is flagged as partial — we don't guess a band).
  - Service charge ÷ 12 (when stated; leasehold only).
  - Ground rent ÷ 12 (same).
  - Deposit amortised over the minimum term — a £3,200 deposit on a
    12-month tenancy reads as '£267 · £3,200 over 12 months'. Skipped
    when either side is unknown (better than amortising over a guess).

## What's NOT in the total (deliberately)

  - Bills. The bills-included flag IS shown — "Included" when true,
    "Extra" when false — but no estimate gets added to the total.
    Bill amounts depend on usage and bedrooms in ways this layer
    can't model honestly; surfacing a fake number would make two
    listings look comparable when they aren't. When bills are
    excluded the total is flagged 'partial'.
  - Tenancy fees text. Illegal under the Tenant Fees Act 2019; the
    existing fineprint section still shows the disclosure for the
    renter to read.

## Two presentations of the same content

  - <Costs> — mobile listing detail shell. Section + serif heading
    + bottom-spacing pattern, matches <PublicRecords> / <Fineprint>
    siblings. Mounted on `/listings/$clusterId` between the portal
    cross-list and the highlights — costs land right where the user
    is thinking about money.
  - <CostsCard> — desktop InfoColumn shell. Same internals wrapped
    in the bordered card chrome to match <AiCard> / <RecordsCard>.
    Mounted between <PriceCard> and <AiCard> in the desktop info
    column.

Both share a single computeCosts() function — the rendered HTML on
desktop is identical to mobile except for the outer wrapper.

## Verification

  - bun run typecheck ✅
  - bun run lint:ci ✅
  - bun run test — 298 / 298 pass (+11 new tests in
    tests/components/costs.test.tsx)

The new tests use renderToStaticMarkup to drive the component end-to-
end and assert on the rendered HTML — covers rent-only, rent + each
cost type, deposit amortisation, deposit-without-term informational
path, bills-included vs not, authority-known-band-unknown partial
total, and the all-fields-present sum. `.tsx` test files needed a
one-line vitest config addition to the include glob — listed inline
with a note for future presentational tests.

`Fineprint` is unchanged — the council-tax table + agent contact
still surface there. The Costs section sits above it as the headline
summary; Fineprint stays as the detail backup.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: c5f108c8-107b-4353-a99e-4367fc687fe6

📥 Commits

Reviewing files that changed from the base of the PR and between 738ae66 and a25bf4d.

📒 Files selected for processing (5)
  • src/components/listing-detail/costs.tsx
  • src/components/listing-detail/desktop-listing-detail.tsx
  • src/routes/listings/$clusterId.tsx
  • tests/components/costs.test.tsx
  • vitest.config.ts

Walkthrough

Adds a new consolidated costs component to listing detail pages that displays rent, council tax, service charge, ground rent, and deposit in an ordered list with a computed monthly total. Integrates the component into both desktop and mobile listing views and includes a comprehensive test suite validating cost calculation and conditional rendering logic.

Changes

Consolidated Costs Component

Layer / File(s) Summary
Costs component specification and documentation
src/components/listing-detail/costs.tsx (lines 1–66)
Module documentation describes the flattened cost list presentation and which items are included in the monthly total versus shown as informational. Public props and internal data shapes for rows and computed totals are defined.
Cost computation pipeline
src/components/listing-detail/costs.tsx (lines 67–166)
computeCosts builds ordered display rows and calculates monthlyTotal with explicit inclusion rules: rent included when present; council tax included only when a band is known (otherwise informational, total flagged partial); service charge and ground rent converted from annual to monthly via rounding; deposit amortised over minimum term when both values valid (else informational); bills rendered as indicators only, with "Extra" flagging total as partial.
Costs rendering components and helpers
src/components/listing-detail/costs.tsx (lines 168–284)
CostsBody renders the computed row list and conditionally displays "True monthly" summary with "(partial)" label and explanatory subtext. Mobile Costs and desktop CostsCard variants reuse computed data and return null when no rows exist. Helper functions provide council-tax band lookup, authority/band formatting, and en-GB locale money formatting.
Desktop listing integration
src/components/listing-detail/desktop-listing-detail.tsx (lines 53, 763–766)
CostsCard is imported and rendered in the desktop InfoColumn stack between PriceCard and AiCard, wired with fineprint and priceMonthly props.
Mobile listing integration
src/routes/listings/$clusterId.tsx (lines 38, 398–404)
Costs is imported and rendered as a new mobile "Consolidated costs" section in the listing detail route immediately after PortalCrossList, wired with fineprint and priceMonthly props.
Test suite and test infrastructure
tests/components/costs.test.tsx, vitest.config.ts (lines 15–22)
Full test suite validates Costs rendering and total calculation across empty output, rent-only totals, council tax inclusion (band known/unknown), annual-to-monthly conversions, deposit amortisation with and without term, bills indicators, and partial total flagging. Vitest configuration is expanded to include .test.tsx files alongside .test.ts for both tests/ and src/ directories.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@timothygithinji
timothygithinji merged commit 9935e4e into main May 28, 2026
4 checks passed
@timothygithinji
timothygithinji deleted the feat/costs-section branch June 21, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant