feat: Tenant Portal + admin de-scope + IA audit fixes (incl. F1–F6 behavior roadmap) - #186
Conversation
…to `items`
The shared PaginatedResponse<T> typed its array field as `data`, but the
backend list services (payments.service, timeline.service) return `items`.
The client `unwrap` transform keys on a top-level `data` property only to
peel the optional ApiEnvelope, so it passed the `{items,total,page,limit}`
object through untouched — and 5 FE reads of `?.data ?? []` then resolved
to `undefined ?? []`, rendering the Payments list and every "Recent
activity" feed empty on production.
Fix: flip the contract array field to `items`, update the 5 paginated reads
(payments-page, finance/staff/tenant dashboards, timeline-feed), and add BE
regression specs asserting the list envelope is keyed by `items`. The two
notification reads keep `.data` — NotificationListResponse is a distinct
shape with a real `data` field. Web-only (packages/contracts + apps/web).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… nav, sign-out reachability, lease date guard, task renter autofill, expense→WO label Sprint F1 of the Property Manager behavior-fix roadmap (workflow-anchored fixes from docs/propertymanager_bugs.txt). All six are contained, verified changes: - F1.1 [Q34/35] Logout returns to the PUBLIC origin. federated-logout now builds post_logout_redirect_uri from x-forwarded-host/host + x-forwarded-proto instead of request.url (which leaked the Next container host behind nginx). KC client prorentallb-web now also has post.logout.redirect.uris="+" (whitelists the redirect) — set additively out-of-band. - F1.2 [Q11/24] Nav "Payments" → "Subscription Payments" (en/ar) and moved beside Subscription in the sidebar; the page is subscription-only. - F1.3 [Q17] Sidebar Sign-out always reachable: dashboard layout is now a fixed app-shell (h-screen/overflow-hidden), <main> scrolls, <nav> scrolls independently. - F1.4 [Q8] Server guard: lease endDate must be strictly after startDate on create() and update() (assertValidDateRange) — the DTO only validated dates in isolation. Regression spec added (api leases 24/24). - F1.5 [Q12] New maintenance-request autofills the renter from the chosen apartment's active lease. - F1.6 [Q16a] Expense→work-order cell shows the #<id> label, not the raw cuid (full WorkOrder.number is F2.1). i18n parity held (en==ar 1023). Verified: web check-types clean, web vitest 30/30, api leases jest 24/24, gitnexus detect_changes footprint matches intent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ers, supervisor deep-links, task filters Sprint F2 of the Property Manager behavior roadmap (workflow W5). F2.1 [Q16] Human-readable WorkOrder.number (decision D4: org-scoped sequential WO-000123). Migration adds WorkOrder.number (nullable→backfill per org by createdAt→NOT NULL + @@unique([orgId,number])). create() assigns the next number under a per-org pg advisory lock (unique index is the backstop). Shared formatWorkOrderNumber() in @repo/contracts; WorkOrderResponse gains number + numberLabel. Shown on the maintenance-request detail Work Orders table and — via a new ExpenseResponse.workOrderNumberLabel (BE joins WorkOrder.number) — on the expenses "Work order" column (replaces the F1.6 #<id> stopgap). Backfills existing rows. F2.2 [Q27] Maintenance "assigned to me". New GET /work-orders/assigned-to-me (WorkOrdersOverviewController + findAssignedToCaller, enriched with request/ apartment/building + active-first ordering). Maintenance dashboard shows a "My work orders" section (assigned WOs at the top, deep-linked to the request). Maintenance-request detail highlights rows assigned to the caller ("Assigned to you" badge; callerSub decoded from the access-token JWT). F2.3 [Q23] Supervisor dashboard deep-links. KPI tiles now link to filtered lists (?status=open|in_progress|resolved, ?priority=urgent) and "Needs attention" rows deep-link to /tasks/:id (were all generic /tasks). Tasks page reads those query params and gained status/priority filter controls. F2.4 [Q12] Reverse renter→apartment autofill in the new-request dialog (renter with one active lease backfills building/floor/apartment), with a ping-pong guard on both directions. i18n parity held (en==ar 1033). Verified: api build EXIT 0 + jest 277 pass/4 skip; web check-types clean, next build EXIT 0, vitest 30/30, eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…permission matrix
Root cause of the partner's Q23 ("clicking Tasks as supervisor bounces to the
dashboard"): proxy.ts (edge middleware) kept an INLINE hand-maintained copy of
the permission map that had drifted from auth/permissions.ts — its `tasks` entry
listed only org_admin + maintenance, omitting supervisor's `readonly`. So the
sidebar (which reads the real matrix) showed the Tasks link, but the edge
redirected supervisor off /dashboard/tasks -> /dashboard. Same class of bug
would recur on any future drift.
Fix: import PERMISSION_MATRIX + getAccess from auth/permissions.ts and gate
directly from it (renters->buildings alias + "unknown sub-area -> allow through"
fallback matching the previous semantics). permissions.ts is a plain object +
pure functions (type-only Role import) so it stays edge-runtime-safe. Divergence
between nav visibility and edge gating is now structurally impossible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ant charge, reports/expense clarity Sprint F3 of the Property Manager behavior roadmap (workflows W4/W7). F3.1 [Q10] Recurring rent invoices (decision D1: monthly, anchored to the lease start day-of-month, generated within 7 days before the due date, idempotent per (lease, billing period), no proration). New recurring-invoices module: generateForLease (UTC-normalized, short-month clamp, 7-day lead window, active+start-date guard, idempotent via new Invoice.billingPeriod + @@unique([leaseId,billingPeriod]) with a P2002 race backstop) -> runForOrg/ runAll; a BullMQ repeatable daily job (06:00) as the scheduler; and POST /recurring-invoices/run (org_admin) on-demand, surfaced as a "Generate rent invoices" button on the Invoices page. F3.2 [Q13] Bill a tenant from a work order (decision D2: opt-in, default OFF). WorkOrder gains chargeToTenant + tenantChargeAmount + tenantChargedAt. On completion (create-as-completed or update-into-completed) with the flag + an amount + not-yet-charged, adds one invoice line item to the apartment's active lease (category other, due +30d) and stamps tenantChargedAt (idempotent; no active lease -> warn, no charge, no throw). FE: charge toggle + amount + badge. F3.3 [Q14/39/40] Reports clarity — income = collected rent (cash basis), net = income - expenses (helper text/tooltips; no math change). F3.4 [Q15] Expense != invoice clarification copy. Two migrations (Invoice.billingPeriod; WorkOrder tenant-charge fields). i18n parity en==ar 1046. Verified: api build EXIT 0 + jest 293 pass/4 skip (incl. 12 recurring + 5 WO-charge); web check-types clean, vitest 30/30, next build EXIT 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…weep, start-date policy Sprint F4 of the Property Manager behavior roadmap (workflow W3). No migration. F4.1 [Q9] True date-overlap lease check. Replaced the old "block if ANY lease is effectively-active now" invariant (which made future/back-to-back leases impossible) with a real end-exclusive range-overlap check (newStart < existingEnd && existingStart < newEnd) against non-terminated leases on the apartment — in create() (active/draft) and update() (when dates/ status change; self-excluded). Allows disjoint future/past + back-to-back; rejects real overlaps. F4.2 [Q9] Apartment status auto-expiry. New apartment-status-sweep module: reverts `occupied` apartments with no effectively-active lease back to `vacant` (occupied-only filter leaves maintenance/unavailable alone). Daily BullMQ repeatable job (07:00) + POST /apartments/status-sweep (org_admin) on-demand trigger (BFF proxy added). F4.3 [Q8, decision D3] Start-date policy. create() rejects a NEW active lease with a past start (400) unless CreateLeaseBody.recordExisting is set; FE adds a "Record an existing lease" checkbox that unlocks back-dating (and sets the flag) + a start-date `min=today` on both lease-create forms. i18n parity en==ar 1052. Verified: api build EXIT 0 + jest 312 pass/4 skip (incl. overlap/policy/sweep specs); web check-types clean, vitest 30/30, next build EXIT 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…feed shows the actor
Sprint F5 of the Property Manager behavior roadmap (workflow W8). No migration.
F5.1 [Q20] Expand notification triggers (were: only support-ticket open/status).
Added best-effort in-app notifications (alongside the existing timeline.emit,
each skips silently when the recipient sub is null) for:
- work_order.assigned → the assignee (WorkOrdersService.create + genuine
reassignment in update; never notifies the actor about themself)
- invoice.issued → the tenant (InvoicesService.create + the F3.1 recurring
auto-generator) via Lease→Renter.renterUserId
- invoice.payment_recorded → the tenant (InvoicePaymentsService.create)
- lease.created → the tenant (LeasesService.create)
Each module now imports NotificationsModule. Role-fanout triggers
(payment-failed→finance, lease-expiring→admin, maintenance-created→supervisors)
are left as documented // TODO F5.1 follow-up (need a shared org-members-by-role
helper + a new expiry sweep).
F5.2 [Q18] Activity feed shows WHO. TimelineService.findForCaller now enriches
each event with actorName by resolving distinct actorId subs via
KeycloakAdminService.getUser (name→email→null), deduped per page + cached
per-sub with a 5-min TTL; KC failure → null, never throws. Contract
TimelineEvent.actorName added; the timeline feed renders "by {actor}" with a
"System" fallback for actor-less/system events; dict wired into both feed
call sites (+ localized the Activity page heading).
i18n parity en==ar 1054. Verified: api build EXIT 0 + full jest 332 pass/4 skip;
web check-types clean, vitest 30/30, next build EXIT 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ble units" showcase
Sprint F6 (final) of the Property Manager behavior roadmap (workflows W10/W11).
No migration.
F6.1 [Q33] Profile page. New /{lang}/dashboard/profile (requireSession, any role)
showing the signed-in identity (name, email, localized role, org name + status,
reusing the existing role/status label maps) + a "Manage account" link to the
Keycloak account console (password/2FA). A "Profile" item was added to the
topbar user dropdown. Fixes the partner's "profile couldn't load" (there was no
/profile route).
F6.2 [Q5] Renter apartment showcase (foundational slice; the full inquiry/
application flow remains a documented v2 follow-up). New GET /available-units
(org-scoped vacant apartments, all member roles incl. tenant, no
building-access filter) → AvailableUnit[]. New "Available units" page + sidebar
nav (permission area availableUnits: tenant full, others readonly). Per-unit
"Express interest" reuses the existing support-ticket create flow (no new data
model) to open a lead ticket referencing the unit.
i18n parity en==ar 1081. Verified: api build EXIT 0 + full jest 335 pass/4 skip
(incl. 3 available-units); web check-types clean, vitest 30/30, next build EXIT 0
(both new routes registered).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…{<Link>}
The topbar user-dropdown "Profile" item using base-ui `render={<Link/>}` threw
Base UI error #31 when the menu opened (crashed the page into the error
boundary). Switched it to a plain DropdownMenuItem with
`onClick={() => router.push(...)}` — the same reliable pattern the sibling
"Sign out" item uses — which navigates without the base-ui render-composition
edge case. Removed the now-unused next/link import.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Group (real Q33 root cause) The topbar avatar → user menu crashed the page into the error boundary the moment it opened (Base UI error #31) — this is the partner's actual Q33 ("clicking profile next to theme changes → this page couldn't load"; the control next to the theme toggle IS this menu). Root cause: DropdownMenuLabel maps to base-ui `Menu.GroupLabel`, which must live inside a `Menu.Group`; it was rendered bare inside DropdownMenuContent, so base-ui threw on open. (The buildings `⋯` menu, which has no label, opened fine — confirming the isolation.) Fix: render the user-name header as a plain styled <div> instead of DropdownMenuLabel (it's a display header, not a semantic group label); dropped the now-unused import. The menu now opens and the Profile item works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tenant Portal (TP1-TP4) + IA1 audit fixes (docs/TENANT-PORTAL-ROADMAP.md). - TP1: admin mints tenant login from create-renter form (email=username, permanent pwd; reuses createUserWithPassword, links renterUserId, no staff seat) - TP2: new /[lang]/portal route group (own chrome) + routing isolation (roles.ts tenant->/portal, proxy.ts tenant<->dashboard both ways, dashboard RSC guard) - TP3: portal content — lease history + apartments, available-units display-only, apartment-scoped Support/Requests, profile - TP4: removed available-units from admin (area/nav/route) + retired express-interest; removed dead TenantDashboard admin branch - IA1: finance timeline domain fix, billing->ORG_ADMIN, supervisor.payments->none, billing page->matrix, dead support-maintenance category removed Zero migrations. i18n parity 1181. api jest 338/4skip, web vitest 30/30. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 104 files, which is 4 over the limit of 100. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (105)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What this PR does
Everything on this team's branch since PR #174 (R1→I1) — the PropertyManager behavior roadmap (F1–F6) plus the new Tenant Portal and cross-role IA/audit fixes. All of it is already deployed and verified live on
prorentallb.cloud(this PR just brings the history onto the integration branch for review/merge).Stacked cleanly on top of the #174 squash (11 commits, zero conflicts). Zero DB migrations.
🏠 Tenant Portal + IA fixes (newest — the focus of this round)
/[lang]/portalwith its own light "Resident portal" chrome (not the admin shell). Tenant-only.MaintenanceRequest— the channel that actually reaches maintenance staff); Profile (identity + Keycloak account console).availableUnitsarea/nav/route entirely and retired express-interest; removed a deadTenantDashboardbranch from the admin router. → No tenant-only surface remains in any staff/admin panel;/dashboard/available-units404s.createUserWithPassword, linksRenter.renterUserId, does not consume a plan staff seat).ROLE_DASHBOARD.tenant → /portal;proxy.tsprotects/portaland bounces tenant↔dashboard both ways; dashboard RSC guard as defense-in-depth.invoice/invoice_payment/expense(was Stripesubscription.*);billingtightened toORG_ADMIN;supervisor.payments → none; billing page routed through the permission matrix; removed the dead-endcategory:'maintenance'support-ticket option.🧰 F1–F6 behavior roadmap (previously deployed, first time on the integration branch)
F1 quick-win correctness · F2 maintenance/work-order workflow (+ the real supervisor-Tasks fix) · F3 recurring rent invoices + WO→tenant charge · F4 leasing correctness (date-overlap, expiry sweep, start-date policy) · F5 notifications & activity actor · F6 profile page + topbar-menu crash fix. Plus the
PaginatedResponseitemsrender fix.Verification
check-types+next buildclean · api build EXIT 0 · i18n parity 1181 == 1181.prorentallb.cloud: tenant login →/portal(lease history renders); admin sidebar has no "Available units";/dashboard/available-units→ 404;/health200. Pre-deploy on a local restart (both EN/AR): tenant↔dashboard isolation both ways, anon-protected, and a tenant submitted a maintenance request end-to-end.Notes for the reviewer
b1f52c8898e6/ webec95540229eb; rollback tagv1-pre-tp-20260721). Merging this PR just records the source history.Dockerfile,apps/api/package.json,package-lock, awebhookscurrentPeriodEnd tweak, andkeycloak-theme/. The prod images were built from those locally. Happy to fold them into a separate infra PR if you'd like them tracked.AccessLevelformaintenance.tasks;maintenancerent/deposit visibility on the org-wide leases list.🤖 Generated with Claude Code