Land Portfolio Manager
Status: Planned
Issue: #160Bundle: Ag Bundle (requireAgPlan(event, PLAN_AG_FARMER); Investor gets higher portfolio caps).
Goal
Give large-farm land managers and farmland investors a single workspace for their portfolio of owned and leased quarter sections: import a list, see them on the map by status, get summary stats, and receive proactive alerts for lease renewals coming up.
User stories
- Farm-management company with 200+ leased quarters across multiple operators needs to track which leases expire when and which year-on-year leases need renegotiation before seeding.
- Off-farm farmland investor with 60 quarters held through a holding company wants weighted-average LSRS productivity, dominant crop mix, and a renewal-alert pipeline that doesn't require a custom CRM build.
- Large-farm operator owns 80 quarters, leases another 120, and wants a single map view where each parcel shows status colour and lease metadata on click.
Feature breakdown
Portfolio import
CSV with required columns:
| Column | Required? | Notes |
|---|---|---|
legal_land | yes | DLS or NTS description; same parser as batch converter |
ownership | yes | one of: owned, leased, prospect |
lease_end_date | if leased | ISO date |
notes | no | free text |
tags | no | comma-separated, used for grouping/filtering |
On import:
- Each row resolves to a parcel via the existing parser
- Coordinates and boundary geometry attached
- Persisted in
app.portfolio_parcelskeyed byuser_id/team_id - Duplicates (same legal_land) merge into the existing row with conflict resolution prompt
Map view
A dedicated workspace route (/app/portfolio) that loads only the user's portfolio parcels on the DLS grid:
- Owned: green fill at 50% opacity
- Leased: blue fill
- Prospect: yellow fill, dashed outline
- Hover: small tooltip with legal land + ownership + lease-end (if applicable)
- Click: detail panel with notes, tags, attachments, lease history
The portfolio loads alongside whichever data layers the user has toggled — LSRS, AAFC crop inventory, oil & gas fields, etc.
Summary stats
A panel showing aggregate stats for the current filter set:
- Total acres (separated by ownership status)
- Weighted-average LSRS productivity score
- Dominant crop mix from the AAFC ACI layer — top 3 crops by area % across the portfolio for the most recent year and the 5-year rolling
- Lease renewals coming up: count due in next 30 / 60 / 90 / 180 days
- Geographic spread: number of townships, number of municipalities
Lease renewal alerts
For each leased parcel with a lease_end_date, the system emits email alerts at:
- 90 days before expiry
- 60 days before expiry
- 30 days before expiry
Alerts roll up daily into a single digest email per user (don't send 50 separate alerts on a day when 50 leases happen to hit the same threshold). The renewal workflow is reused from the existing email infrastructure (Resend).
Per-parcel attachments
Each parcel row supports file attachments — lease PDF, title PDF, crop insurance copy, etc. v1 is simple file storage (S3-backed). v2 layer would add an OCR pass to extract key fields from uploaded leases (deferred).
Database
New tables:
app.portfolio_parcels— user/team-keyed parcel rows with the imported columnsapp.portfolio_attachments— file metadata (S3 key, content type, size)app.portfolio_renewals_sent— idempotency table preventing duplicate alert sends
API surface
POST /api/portfolio/import # CSV upload (multipart)
GET /api/portfolio # List user's portfolio with filters
GET /api/portfolio/[id] # Single parcel detail
PATCH /api/portfolio/[id] # Update notes/tags/lease metadata
DELETE /api/portfolio/[id] # Remove from portfolio
GET /api/portfolio/stats # Aggregate stats
POST /api/portfolio/[id]/attachments # Attach a file
All endpoints behind requireAgPlan(event, PLAN_AG_FARMER).
Entitlement-driven limits
| Tier | Max portfolio parcels | Attachments per parcel | Renewal alerts |
|---|---|---|---|
| Lite | 10 | 0 | no |
| Farmer | 250 | 10 | yes |
| Investor | Unlimited | Unlimited | yes |
Limits enforced via the same pattern as existing checkQuickSavesLimit / checkProjectLimit helpers in server/utils/checkUsageLimit.js.
UI
A new route at /app/portfolio rendered through the existing app layout (no new Vue layout work). Components:
app/components/portfolio/PortfolioMap.vue— map viewapp/components/portfolio/PortfolioTable.vue— table view (sort/filter)app/components/portfolio/PortfolioStats.vue— summary stats panelapp/components/portfolio/PortfolioImport.vue— CSV upload dialogapp/components/portfolio/ParcelDetailPanel.vue— parcel detail flyoutapp/components/portfolio/LeaseRenewalAlerts.vue— alerts settings
Acceptance criteria progress
- Schema migration for portfolio tables
- CSV import endpoint + parser reuse
- Map view with ownership colouring
- Summary stats endpoint + panel
- Lease renewal email cron + idempotency
- Attachment upload + storage
- Tier-gated limits enforced server-side
- UI components + route at
/app/portfolio - Behind
requireAgPlan(event, PLAN_AG_FARMER)
Related features
- LSRS Productivity Score — surfaces weighted-avg in portfolio stats
- AAFC Annual Crop Inventory — surfaces dominant crop mix in portfolio stats
- One-click Parcel Report — generated per-parcel for any portfolio row on demand
- Territory & Prospecting — Investor-tier feature that operates on parcel polygons including portfolio entries