Docs index
Feature Reference

Land Portfolio Manager

Status: Planned Issue: #160Bundle: Ag Bundle (requireAgPlan(event, PLAN_AG_FARMER); Investor gets higher portfolio caps).

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:

ColumnRequired?Notes
legal_landyesDLS or NTS description; same parser as batch converter
ownershipyesone of: owned, leased, prospect
lease_end_dateif leasedISO date
notesnofree text
tagsnocomma-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_parcels keyed by user_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 columns
  • app.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

TierMax portfolio parcelsAttachments per parcelRenewal alerts
Lite100no
Farmer25010yes
InvestorUnlimitedUnlimitedyes

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 view
  • app/components/portfolio/PortfolioTable.vue — table view (sort/filter)
  • app/components/portfolio/PortfolioStats.vue — summary stats panel
  • app/components/portfolio/PortfolioImport.vue — CSV upload dialog
  • app/components/portfolio/ParcelDetailPanel.vue — parcel detail flyout
  • app/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)