# MVP Backoffice (CWS) — Android App Feature Specification

**Version:** 260822b (August 22, 2026)
**Base URL:** `https://mvp.catalyticwatersolutions.com/api`
**Auth:** JWT Bearer token in `Authorization` header
**Platform detection:** Send `platform: "android"` on login

---

## Table of Contents

1. [Authentication](#1-authentication)
2. [Customer Profile & Account](#2-customer-profile--account)
3. [Service Addresses](#3-service-addresses)
4. [Equipment](#4-equipment)
5. [Appointments](#5-appointments)
6. [Invoices](#6-invoices)
7. [Estimates](#7-estimates)
8. [Contracts (NEW)](#8-contracts-new)
9. [Deliveries (NEW)](#9-deliveries-new)
10. [Water Tests](#10-water-tests)
11. [Salt Delivery Requests](#11-salt-delivery-requests)
12. [IoT Devices](#12-iot-devices)
13. [Push Notifications](#13-push-notifications)
14. [Dashboard Poll](#14-dashboard-poll)
15. [Data Models](#15-data-models)

---

## 1. Authentication

### POST `/auth/login`
```json
Request:  { "email": "...", "password": "...", "platform": "android" }
Response: { "access_token": "...", "refresh_token": "...", "role": "customer|technician|admin", "user_id": 123, "customer_id": 456, "expires_in": 3600 }
```

### POST `/auth/refresh`
```json
Request:  { "refresh_token": "..." }
Response: { "access_token": "...", "refresh_token": "...", "expires_in": 3600 }
```

### POST `/auth/logout`
```json
Header: Authorization: Bearer <token>
Response: { "message": "Logged out" }
```

---

## 2. Customer Profile & Account

### GET `/customer/me`
Returns the authenticated customer's profile.
```json
Response: {
  "customer_id": 1,
  "first_name": "John",
  "last_name": "Doe",
  "phone": "(309) 555-1234",
  "email": "john@example.com",
  "service_address": "123 Main St",
  "service_city": "Springfield",
  "service_state": "IL",
  "service_zip": "62701",
  "notes": "...",
  "override_enabled": false
}
```

### PUT `/customer/me`
Update own profile fields (phone, email, service_address, service_city, service_state, service_zip, notes).

### PUT `/customer/password`
```json
Request: { "current_password": "...", "new_password": "..." }
```
Invalidates all refresh tokens after password change.

---

## 3. Service Addresses (NEW)

Customers can have multiple service addresses (e.g., home, rental property).

### GET `/customer/service-addresses`
```json
Response: [
  {
    "service_address_id": 1,
    "label": "Home",
    "service_address": "123 Main St",
    "service_city": "Springfield",
    "service_state": "IL",
    "service_zip": "62701",
    "is_primary": true
  }
]
```

### POST `/customer/service-addresses`
```json
Request: { "label": "Rental", "service_address": "456 Oak Ave", "service_city": "Decatur", "service_state": "IL", "service_zip": "62521", "is_primary": false }
```

### PUT `/customer/service-addresses/{id}`
Update any address field.

### DELETE `/customer/service-addresses/{id}`
Deletes address. Any equipment assigned to it becomes unassigned.

---

## 4. Equipment

### GET `/customer/equipment`
All active equipment for the customer, ordered by `next_service_due ASC`.
```json
Response: [
  {
    "equipment_id": 1,
    "type_name": "Water Softener",
    "category": "water",
    "model": "WS-480",
    "install_date": "2024-01-15",
    "service_interval_days": 90,
    "last_service_date": "2026-06-15",
    "next_service_due": "2026-09-13",
    "days_until_due": 20,
    "notes": "Salt-based system"
  }
]
```

### GET `/customer/equipment/{id}`
Single equipment detail (same fields as above).

### GET `/customer/equipment/{id}/history`
```json
Response: [
  {
    "record_id": 1,
    "service_date": "2026-06-15",
    "service_type": "Routine Maintenance",
    "next_service_due": "2026-09-13",
    "notes": "Replaced filter",
    "logged_by": "technician",
    "technician": "Mike Smith"
  }
]
```

---

## 5. Appointments

### GET `/customer/appointments`
Non-cancelled appointments for the customer.
```json
Response: [
  {
    "appointment_id": 1,
    "service_type": "Routine Maintenance",
    "requested_date": "2026-08-25",
    "requested_window": "Morning",
    "confirmed_date": "2026-08-25",
    "confirmed_time": "09:00",
    "status": "confirmed",
    "customer_notes": "Gate code 1234",
    "salt_delivery": false,
    "oxyblast": false,
    "equipment": [
      { "equipment_id": 1, "type_name": "Water Softener", "model": "WS-480" }
    ]
  }
]
```

### GET `/customer/appointments/service-types`
Available service types (customer-requestable only).
```json
Response: [
  { "type_id": 1, "name": "Routine Maintenance", "min_days_out": 3 },
  { "type_id": 2, "name": "Emergency Repair", "min_days_out": 0 }
]
```

### POST `/customer/appointments`
Request a new appointment.
```json
Request: {
  "service_type_id": 1,
  "requested_date": "2026-08-25",
  "requested_window": "Morning",
  "customer_notes": "Gate code 1234",
  "equipment_ids": [1, 2],
  "salt_delivery": false,
  "oxyblast": false,
  "service_address_id": 1
}
Response: { "message": "Appointment requested", "appointment_id": 1 }
```
**Guards:**
- `do_not_service` flag blocks booking
- Must have at least one piece of equipment
- Only one pending request at a time
- `min_days_out` enforced per service type
- Max 14 days out

### DELETE `/customer/appointments/{id}`
Cancel a pending request. Only `status='pending'` allowed.

---

## 6. Invoices

### GET `/customer/invoices`
Non-void invoices for the customer, ordered by `created_at DESC`.
```json
Response: [
  {
    "invoice_id": 1,
    "invoice_number": "INV-0042",
    "status": "sent",
    "issue_date": "2026-08-01",
    "subtotal": 150.00,
    "tax_amount": 12.00,
    "total": 162.00,
    "appointment_id": 1,
    "created_at": "2026-08-01T10:00:00",
    "last_payment_date": null
  }
]
```

### GET `/customer/invoices/{id}`
Full invoice detail with line items and payments.

### GET `/customer/invoices/{id}/pdf`
Returns PDF binary stream.

---

## 7. Estimates

### GET `/customer/estimates`
Non-draft estimates for the customer.

### GET `/customer/estimates/{id}`
Full estimate detail with lines and customer info.

### POST `/customer/estimates/{id}/respond`
```json
Request: { "action": "approve", "notes": "Looks good" }
```
Only `status='sent'` estimates can be responded to. Checks expiry date.

---

## 8. Contracts (NEW)

Service contracts link equipment, service types, pricing, and billing cycles.

### GET `/customer/contracts`
Active and expired contracts for the customer.
```json
Response: [
  {
    "contract_id": 1,
    "contract_number": "SC-0001",
    "name": "Annual Water Treatment Plan",
    "status": "active",
    "start_date": "2026-01-01",
    "end_date": "2026-12-31",
    "auto_renew": true,
    "frequency": "quarterly",
    "billing_cycle": "quarterly",
    "cycle_price": 450.00,
    "created_at": "2026-01-01T10:00:00"
  }
]
```

### GET `/customer/contracts/{id}`
Full contract detail with equipment and service types.
```json
Response: {
  "contract_id": 1,
  "contract_number": "SC-0001",
  "name": "Annual Water Treatment Plan",
  "status": "active",
  "start_date": "2026-01-01",
  "end_date": "2026-12-31",
  "auto_renew": true,
  "renew_term_months": 12,
  "frequency": "quarterly",
  "custom_interval_days": null,
  "visits_per_cycle": 1,
  "billing_cycle": "quarterly",
  "cycle_price": 450.00,
  "per_visit_price": null,
  "discount_percent": null,
  "notes": "...",
  "equipment": [
    { "equipment_id": 1, "type_name": "Water Softener", "model": "WS-480" }
  ],
  "service_types": [
    { "service_type_id": 1, "name": "Routine Maintenance", "included_visits": 1 }
  ]
}
```

**Status values:** `draft`, `active`, `expired`, `cancelled`

---

## 9. Deliveries (NEW)

Product delivery scheduling and tracking (e.g., salt, chemicals).

### 9a. Customer View

There is no dedicated customer deliveries endpoint yet. Deliveries are visible in the admin backoffice under the customer profile. The Android app should display delivery information if provided by the office.

### 9b. Technician View

#### GET `/tech/deliveries`
Deliveries assigned to the current technician (status: `assigned` or `in_progress`).
```json
Response: [
  {
    "delivery_id": 1,
    "delivery_number": "DEL-0001",
    "customer_id": 456,
    "recipient_name": "John Doe",
    "delivery_address": "123 Main St",
    "delivery_city": "Springfield",
    "delivery_state": "IL",
    "delivery_zip": "62701",
    "status": "assigned",
    "scheduled_date": "2026-08-25",
    "scheduled_window": "morning",
    "notes": "Leave at back door",
    "customer_name": "John Doe",
    "customer_phone": "(309) 555-1234",
    "lines": [
      {
        "line_id": 1,
        "product_id": 1,
        "product_name": "34% Hydrogen Peroxide",
        "container_size": "5 gal",
        "unit_label": "pail",
        "qty_ordered": 2,
        "qty_delivered": 0,
        "empties_returned": 0
      }
    ]
  }
]
```

#### POST `/tech/deliveries/{id}/complete`
Complete a delivery with GPS and optional signature.
```json
Request: {
  "recipient_name": "John Doe",
  "delivery_address": "123 Main St",
  "delivery_city": "Springfield",
  "delivery_state": "IL",
  "delivery_zip": "62701",
  "signature_path": "/uploads/signatures/del_1_sig.png",
  "gps_lat": 39.7817,
  "gps_lng": -89.6501,
  "gps_accuracy_m": 12.5,
  "gps_captured_at": "2026-08-25T10:30:00Z",
  "lines": [
    { "line_id": 1, "qty_delivered": 2, "empties_returned": 2 }
  ]
}
```

### 9c. Delivery Status Flow
`pending` → `assigned` → `in_progress` → `completed`
Also: `cancelled`

### 9d. Delivery Product Catalog (Admin only)

#### GET `/admin/delivery-products`
```json
Response: [
  { "product_id": 1, "name": "34% Hydrogen Peroxide", "container_size": "5 gal", "unit_label": "pail", "is_active": 1 },
  { "product_id": 2, "name": "Liquid Chlorine (Sodium Hypochlorite)", "container_size": "5 gal", "unit_label": "pail", "is_active": 1 },
  { "product_id": 3, "name": "34% Hydrogen Peroxide", "container_size": "15 gal", "unit_label": "drum", "is_active": 1 },
  { "product_id": 4, "name": "Chlorine Tablets (Trichlor)", "container_size": "50 lb", "unit_label": "bucket", "is_active": 1 }
]
```

---

## 10. Water Tests

### GET `/customer/water-test`
```json
Response: {
  "current": {
    "test_id": 5,
    "label": "Annual Test 2026",
    "test_date": "2026-07-01",
    "is_current": true,
    "pdf_url": "/uploads/water-tests/test_5.pdf"
  },
  "history": [
    { "test_id": 5, "label": "Annual Test 2026", "test_date": "2026-07-01", "is_current": true, "pdf_url": "..." },
    { "test_id": 3, "label": "Initial Test", "test_date": "2024-01-15", "is_current": false, "pdf_url": "..." }
  ]
}
```

---

## 11. Salt Delivery Requests

### POST `/customer/salt-delivery`
```json
Request: {
  "bags": 6,
  "requested_date": "2026-09-01",
  "requested_window": "Morning",
  "notes": "Leave by garage"
}
Response: { "message": "Salt delivery requested", "appointment_id": 12, "bags": 6, "requested_date": "2026-09-01" }
```
**Guards:** Min 4 bags, date must be 4+ days out and a weekday. Auto-creates a pending appointment with service type "Salt Delivery" and sends email notification to office.

---

## 12. IoT Devices

### GET `/customer/iot/devices`
List devices belonging to the customer.

### GET `/customer/iot/devices/{id}`
Single device with latest readings.

### GET `/customer/iot/devices/{id}/readings`
Time-series readings data.

### POST `/customer/iot/devices/{id}/alerts/acknowledge`
Acknowledge an alert.

---

## 13. Push Notifications

### POST `/customer/push/subscribe`
Register FCM push subscription.
```json
Request: { "endpoint": "...", "p256dh": "...", "auth": "...", "user_agent": "..." }
```

### DELETE `/customer/push/unsubscribe`
Remove push subscription.

### GET `/customer/push/vapid-public-key`
Returns VAPID public key for web push (may not apply to native Android).

---

## 14. Dashboard Poll

### GET `/customer/poll`
Single call to get all dashboard state.
```json
Response: {
  "appointments": [...],
  "unpaid_count": 2,
  "equipment": [...],
  "notifications": [...]
}
```

### POST `/customer/notifications/{id}/ack`
Acknowledge a notification.

---

## 15. Data Models

### Delivery
| Field | Type | Description |
|-------|------|-------------|
| delivery_id | int | Primary key |
| delivery_number | string | Auto-generated (DEL-XXXX) |
| customer_id | int? | Nullable for non-customer deliveries |
| recipient_name | string? | Name of person receiving (used when no customer or different from customer) |
| delivery_address | string? | Override address (falls back to customer's service_address) |
| delivery_city | string? | |
| delivery_state | string? | |
| delivery_zip | string? |
| technician_id | int? | Assigned tech |
| status | string | pending, assigned, in_progress, completed, cancelled |
| scheduled_date | date? | |
| scheduled_window | string? | morning, afternoon, either |
| signature_path | string? | Path to signature image |
| gps_lat | decimal? | GPS latitude at completion |
| gps_lng | decimal? | GPS longitude at completion |
| gps_accuracy_m | float? | GPS accuracy in meters |
| gps_captured_at | datetime? | When GPS was captured |
| notes | text? | |
| lines | array | Array of line items |

### Delivery Line
| Field | Type | Description |
|-------|------|-------------|
| line_id | int | Primary key |
| delivery_id | int | FK to deliveries |
| product_id | int | FK to delivery_products |
| product_name | string | From JOIN |
| container_size | string? | From JOIN |
| unit_label | string? | From JOIN |
| qty_ordered | int | |
| qty_delivered | int | |
| empties_returned | int | |

### Delivery Product
| Field | Type | Description |
|-------|------|-------------|
| product_id | int | Primary key |
| name | string | e.g., "34% Hydrogen Peroxide" |
| container_size | string? | e.g., "5 gal", "15 gal", "50 lb" |
| unit_label | string? | e.g., "pail", "drum", "bucket" |
| is_active | bool | |

### Contract
| Field | Type | Description |
|-------|------|-------------|
| contract_id | int | Primary key |
| contract_number | string | Auto-generated (SC-XXXX) |
| customer_id | int | FK to customers |
| name | string | Contract name |
| status | string | draft, active, expired, cancelled |
| start_date | date | |
| end_date | date? | Null if open-ended |
| auto_renew | bool | |
| renew_term_months | int | Months to extend on renewal |
| frequency | string? | weekly, biweekly, monthly, quarterly, semi_annual, annual, custom |
| custom_interval_days | int? | For custom frequency |
| visits_per_cycle | int? | |
| billing_cycle | string? | monthly, quarterly, semi_annual, annual |
| cycle_price | decimal? | Price per billing cycle |
| per_visit_price | decimal? | Price per visit |
| discount_percent | decimal? | |
| notes | text? | |
| created_at | datetime | |

### Lead (from booking form)
| Field | Type | Description |
|-------|------|-------------|
| lead_id | int | Primary key |
| first_name | string | |
| last_name | string | |
| full_name | string | Computed (first + last) |
| phone | string | |
| email | string? | |
| service_type | string? | |
| address | string? | |
| city | string? | |
| state | string? | |
| preferred_date | string? | |
| preferred_window | string? | |
| referral | string? | How they found us |
| notes | text? | |
| status | string | new, contacted, scheduled, converted, lost |
| assigned_to | int? | User ID of assigned staff |
| assigned_to_name | string? | Computed from JOIN |
| converted | bool | Whether converted to customer |
| submitted_at | datetime | |

### Service Address
| Field | Type | Description |
|-------|------|-------------|
| service_address_id | int | Primary key |
| customer_id | int | FK to customers |
| label | string | e.g., "Home", "Rental", "Office" |
| service_address | string? | |
| service_city | string? | |
| service_state | string? | |
| service_zip | string? | |
| is_primary | bool | |

---

## Recent Changes (Since Last Spec)

### v260822b (August 22, 2026)

1. **Deliveries System** — Full delivery scheduling with:
   - Nullable `customer_id` for non-customer deliveries
   - Address override fields (`delivery_address`, `delivery_city`, `delivery_state`, `delivery_zip`)
   - Product catalog (4 seed products: Hydrogen Peroxide, Liquid Chlorine, Chlorine Tablets)
   - Delivery lines with qty_ordered, qty_delivered, empties_returned
   - Tech can complete with GPS capture + signature
   - Customer profile shows delivery history

2. **Leads/Prospects Management** — New lead pipeline:
   - Status tracking: new → contacted → scheduled → converted | lost
   - Convert to customer with one click (creates user + customer)
   - Assign leads to staff members
   - Search and filter by status

3. **Contracts** — Service contract management:
   - Equipment and service type associations
   - Billing cycles and pricing
   - Auto-renewal support
   - Customer can view contracts via `/customer/contracts`

4. **Service Addresses** — Multi-address support:
   - Customers can have multiple labeled service addresses
   - Equipment can be assigned to specific addresses
   - Deliveries can use address override

5. **Email Template Redesign** — New header/footer:
   - Logo left-aligned with company name
   - Both phone numbers: (309) 258-2582 and (309) 643-1342
   - Customer portal link: https://mvp.catalyticwatersolutions.com
   - Professional light color scheme

6. **Customer Portal URL** — Changed to `https://mvp.catalyticwatersolutions.com`

---

## Technician Endpoints (Quick Reference)

For the Android tech app, here are the key endpoints:

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/tech/schedule` | GET | Upcoming service jobs |
| `/tech/service` | POST | Log a service visit |
| `/tech/appointments` | GET | Confirmed schedule |
| `/tech/appointments/{id}` | GET | Appointment detail |
| `/tech/appointments/{id}/claim` | POST | Self-assign |
| `/tech/appointments/{id}/on-my-way` | POST | Notify customer |
| `/tech/appointments/{id}` | PUT | Update/edit appointment |
| `/tech/deliveries` | GET | Assigned deliveries |
| `/tech/deliveries/{id}/complete` | POST | Complete delivery |
| `/tech/customers` | GET | Search customers |
| `/tech/customers/{id}` | GET | Customer detail |
| `/tech/customers/{id}/service-history` | GET | Service records |
| `/tech/invoices` | GET | List invoices |
| `/tech/invoices/{id}` | GET | Invoice detail |
| `/tech/invoices/{id}/payment` | POST | Record payment |
| `/tech/contracts` | GET | Contracts for assigned customers |
| `/tech/clock-time` | GET | Clock status |
| `/tech/clock-time` | POST | Clock in |
| `/tech/clock-time/{id}` | PUT | Clock out |
| `/tech/device-token` | POST | Register FCM token |

---

## Admin Endpoints (Quick Reference)

For completeness, admin endpoints are at `/admin/*`. Key ones:

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/admin/deliveries` | GET/POST | List/create deliveries |
| `/admin/deliveries/{id}` | GET/PUT/DELETE | Delivery CRUD |
| `/admin/deliveries/{id}/assign` | POST | Assign technician |
| `/admin/deliveries/{id}/complete` | POST | Mark complete |
| `/admin/delivery-products` | GET/POST | Product catalog |
| `/admin/leads` | GET | List leads |
| `/admin/leads/{id}` | GET/PUT/DELETE | Lead CRUD |
| `/admin/leads/{id}/convert` | POST | Convert to customer |
| `/admin/contracts` | GET/POST | List/create contracts |
| `/admin/contracts/{id}` | GET/PUT/DELETE | Contract CRUD |
| `/admin/contracts/{id}/activate` | POST | Activate draft |
| `/admin/contracts/{id}/cancel` | POST | Cancel contract |
| `/admin/contracts/{id}/renew` | POST | Renew contract |

---

## Notes for Android Developer

1. **Auth token storage:** Store `access_token` and `refresh_token` securely (EncryptedSharedPreferences). Auto-refresh when `expires_in` nears expiry.

2. **Platform detection:** Send `"platform": "android"` in the login request body. The backend stores this in `last_login_platform`.

3. **GPS capture:** When completing deliveries or clocking in/out, capture GPS using Android's FusedLocationProvider. Send `gps_lat`, `gps_lng`, `gps_accuracy_m`, `gps_captured_at`.

4. **Signature capture:** For delivery completion, capture a signature using a Canvas view and upload as PNG to `/admin/customers/{id}/images` or include the base64 path.

5. **Push notifications:** Register FCM token via `POST /tech/device-token` (tech) or `POST /customer/push/subscribe` (customer web push — may need adaptation for native Android FCM).

6. **Offline handling:** The app requires a live connection (no offline caching per spec). Show appropriate error states.

7. **Date format:** All dates are `YYYY-MM-DD`. Datetimes are ISO 8601.

8. **Error responses:** All errors return `{ "error": "message" }` with appropriate HTTP status code.

9. **Currency:** All prices are in USD, displayed with 2 decimal places.

10. **Timezone:** All times are in America/Chicago (CDT/CST).
