# Self-Service Portal — Product Specification

A read-only web portal where lot owners can view their billing history, download invoices, and track their utility consumption — without requiring a full admin account.

---

## Goals

- Zero-friction access: no password registration, OTP only
- Works for owners across multiple sites and service providers
- Mobile-first responsive layout
- Frontend framework: Vue JS (consistent with existing stack)

---

## Authentication Flow

### Step 1 — Email entry

User enters their email address. The system looks up `lot_owners` where `email`, `alternate_email_1`, or `alternate_email_2` matches (case-insensitive). If no match is found, show a generic message: *"If this email is registered, you will receive a code shortly."* (never confirm or deny existence).

### Step 2 — OTP dispatch

- Generate a 6-digit numeric OTP
- Valid for **10 minutes**, single-use
- Sent to the email address the user typed (not the primary email on the record)
- Store: hashed OTP, email, `expires_at`, `used_at` (null until consumed)
- Rate-limit: max 3 OTP requests per email per 15-minute window

### Step 3 — OTP verification

User enters the code. On success:
- Mark OTP as used (`used_at = now()`)
- Issue a short-lived JWT (role: `portal_user`, 2-hour expiry)
- Record a login log entry (email, IP, user agent, timestamp, success/fail)
- Redirect to the bill history view

On failure: increment attempt counter. Lock after 5 failed attempts for 15 minutes.

### Step 4 — Session

JWT is stored in memory (not localStorage) to reduce XSS risk. On expiry, redirect back to email entry — no silent refresh. Portal has no persistent session concept.

---

## Data Model — Lot Resolution

After authentication, resolve all lots linked to the verified email:

```
lot_owners
  ├── email / alternate_email_1 / alternate_email_2 matches login email
  └── has_many lots (via lot_owner_id)
        ├── site_id  → Site → service_provider_id
        └── service_type_id
```

A single email may resolve to lots across **multiple sites and multiple service providers**. All are surfaced.

---

## Portal Views

### 1. Bill History (main view)

**Filters (persistent in URL params):**

| Control | Type | Behaviour |
|---|---|---|
| Site | Dropdown | Lists all sites the user has at least one lot on. Default: first site alphabetically |
| Service type | Tabs | Water / Electricity / Gas — only show tabs for types that have bills |
| Date range | Optional date picker | Defaults to last 12 months |

**Bill list (paginated — 12 per page):**

Each row:
- Invoice number (`id`)
- Billing period (`survey_start_date` → `survey_end_date`)
- Units consumed (formatted with `billingUnitLabel`)
- Net amount + tax (formatted with `formatMoney`)
- Tax label + percentage (if `tax_percentage > 0`)
- Due date
- Download button (PDF) / View button (opens in-browser)

Pagination: standard prev / next with page numbers. URL-param driven so links are shareable.

---

### 2. Summary Cards

Shown above the bill list, scoped to the currently selected site + service type.

| Card | Calculation | Notes |
|---|---|---|
| **Average bill** | Mean of `net_bill_value + tax_value` across last 12 months | |
| **Average consumption** | Mean of `units_consumed` across last 12 months | Label with `billingUnitLabel` |
| **Last 30-day estimate** | Most recent closed bill's `units_consumed` ÷ billing period days × 30 | Labelled as "estimate" |
| **Current meter reading** | `current_unit` from the active open session (if one exists) | Show date of last reading. Hide card if no open session |
| **Last bill total** | Most recent `net_bill_value + tax_value` | With date |

---

### 3. Bill Detail / PDF Viewer

Triggered by "View" button. Opens the existing `siteServiceTemplate` PDF rendered inline in an `<iframe>` or via a PDF.js viewer. Same PDF as the downloaded version — no separate template needed.

"Download" button triggers the existing PDF download endpoint directly.

---

### 4. Profile

Read-only view of the lot owner's details as stored:
- Name
- Address
- Primary email, alternate emails
- All linked lot numbers with their site

**Update request:** A simple form to flag a correction (name, address, contact detail). Submits a notification email to the service provider admin — does **not** update the record directly (admin reviews and applies). This avoids self-service data integrity issues.

---

### 5. Notification Preferences

| Preference | Default | Notes |
|---|---|---|
| Receive bill email notifications | On | Opt-out only — admin can override |

Stored on `lot_owners` as a `notification_opt_out boolean` (new column, default false). The existing mail dispatch code checks this flag before sending.

---

### 6. Login Log

Last 10 login events for the authenticated user:
- Date / time
- IP address
- User agent (browser/device summary)
- Status (success / failed)

Shown on a "Security" or "Account activity" page. Helps users spot unexpected access.

---

## Additional Considerations

### Multi-lot, multi-site owners

If the resolved email has lots on more than one site, show a **"You have bills across X sites"** banner on first login and default the site dropdown to the site with the most recent bill.

### OTP email — bill deep link

When an OTP is triggered from a notification email's "View bill" link (future), the OTP email can carry a `redirect` param so the user lands directly on that specific invoice after auth.

### Empty states

- No bills found for selected filters: *"No invoices found for this period."*
- No lots found for email: handled at auth step (silent)
- Open session not yet closed: summary cards show last closed bill only; current reading card shows live meter value

### Security

- All portal routes sit under a separate `/portal` prefix with `portal_user` middleware — zero overlap with admin JWT guards
- OTP tokens stored hashed (bcrypt or SHA-256)
- PDF download endpoint validates that the requested `archived_session_id` belongs to a lot owned by the authenticated portal user — no IDOR
- Login log IP captured server-side (not trusted from header without proxy config)

---

## Backend Work Required

### New DB

| Table | Purpose |
|---|---|
| `portal_otps` | `id, email, otp_hash, expires_at, used_at, ip_address, created_at` |
| `portal_login_logs` | `id, email, ip_address, user_agent, status (success/fail), created_at` |
| `lot_owners.notification_opt_out` | Boolean column, default false |

### New API Routes (`/api/portal/*`)

| Method | Route | Description |
|---|---|---|
| `POST` | `/portal/request-otp` | Trigger OTP for email |
| `POST` | `/portal/verify-otp` | Verify OTP, return JWT |
| `GET` | `/portal/bills` | Paginated bill list (filters: site_id, service_type_id, from, to) |
| `GET` | `/portal/bills/{id}/pdf` | Stream PDF — ownership validated |
| `GET` | `/portal/summary` | Summary card data for selected site + service type |
| `GET` | `/portal/lots` | All resolved lots + sites for the authenticated email |
| `GET` | `/portal/login-log` | Last 10 login events |
| `PUT` | `/portal/notification-preferences` | Update opt-out flag |
| `POST` | `/portal/profile-update-request` | Submit correction request to admin |

### Middleware

- `portal.auth` — validates `portal_user` JWT, attaches resolved lot IDs to request
- `portal.rate-limit` — applies per-email OTP rate limiting

---

## Out of Scope (v1)

- Payment processing / balance tracking
- Real-time meter reading submission by lot owner
- Admin-facing portal management UI (handled via existing admin panel)
- Push notifications / SMS OTP
- Multi-language support
