# WBMS Flutter App — Meter Photo Scan (AI-assisted reading)

Lets a meter reader photograph a water meter; the backend uses Claude vision to
read the **meter number** and **current reading**, checks it against this
company's records, and returns a **draft** the reader confirms before submitting.

> The reader always confirms. The scan **never** submits a reading on its own —
> it pre-fills the form, the reader reviews/edits, then submits as normal.

The Claude API key lives **only on the Laravel backend**. The app never calls
Claude directly and never holds the key.

---

## Flow

```
1. Reader opens a client / route, taps "Scan meter", takes a photo.
2. App POSTs the photo  → POST /api/v1/meter-reading/scan   (Bearer token, multipart)
3. Backend → Claude vision → { meter_number, reading_value, confidence } → looks up
   the meter in this company → returns a DRAFT (matched client, previous reading,
   estimated usage, warnings).
4. App shows the draft pre-filled. Reader reviews, fixes anything flagged, confirms.
5. App submits the confirmed values → POST /api/v1/meter-reading/submit  (existing flow).
```

---

## Endpoint

```
POST /api/v1/meter-reading/scan
Authorization: Bearer <token>
Accept: application/json
Content-Type: multipart/form-data
```
Roles: `meter_reader`, `company_admin` (others get `403`).

**Body (multipart):**
| Field | Type | Notes |
|-------|------|-------|
| `image` | file | JPEG / PNG / WebP, ≤ 8 MB. The meter photo. |

**Success (200) — a draft, not a submission:**
```json
{
  "extracted": {
    "meter_number": "WM-12345",
    "reading_value": 1234.56,
    "confidence": "high",
    "notes": "clear"
  },
  "match": {
    "found": true,
    "client": {
      "id": 1,
      "account_number": "ACC-1-000001",
      "name": "Juan Dela Cruz",
      "meter_number": "WM-12345",
      "status": "active"
    }
  },
  "previous_reading": 1229.31,
  "estimated_usage": 5.25,
  "warnings": [],
  "requires_confirmation": true
}
```

When something is uncertain, fields are `null` and `warnings` explains why:
```json
{
  "extracted": { "meter_number": null, "reading_value": 1180.0, "confidence": "low", "notes": "glare on dials" },
  "match": { "found": false, "client": null },
  "previous_reading": null,
  "estimated_usage": null,
  "warnings": [
    "No meter number could be read from the photo.",
    "Low/medium confidence (low) — verify the reading carefully."
  ],
  "requires_confirmation": true
}
```

**Other responses:**
| Status | Meaning | App should… |
|--------|---------|-------------|
| 401 | Missing/expired token | Re-login |
| 403 | Role not allowed | Hide the scan feature for this role |
| 422 | Invalid file, or image not interpretable | Ask for a clearer photo / manual entry |
| 502 | Vision service error | Offer retry or manual entry |
| 503 | Scanning not configured on server | Hide the scan button; fall back to manual entry |

---

## Confirmation UI (required)

Pre-fill the reading form from `extracted` + `match`, then make the reader confirm:

- If `match.found` is **false** → don't let them submit against a guessed client;
  show "meter not recognized — pick the client manually" (or search by account).
- Show every string in `warnings` prominently.
- If `confidence` is `low`/`medium`, or `estimated_usage` is negative, require an
  explicit tap to confirm the reading is correct.
- Let the reader edit `reading_value` (and the client) before submitting — the AI
  output is a suggestion, not a commitment.

Then submit the **confirmed** values via the normal `POST /api/v1/meter-reading/submit`.

---

## Flutter (Dio multipart)

```dart
Future<MeterScanDraft> scanMeter(File photo) async {
  final form = FormData.fromMap({
    'image': await MultipartFile.fromFile(
      photo.path,
      filename: 'meter.jpg',
      // contentType: MediaType('image', 'jpeg'),  // from package:http_parser
    ),
  });
  final res = await dio.post('/meter-reading/scan', data: form);
  return MeterScanDraft.fromJson(res.data);
}
```

Capture the photo with `image_picker` (`ImageSource.camera`) or `camera`. Compress
to a reasonable size before upload (e.g. `flutter_image_compress`, long edge
~1600px) — smaller images upload faster and cost fewer tokens, with no accuracy
loss for legible meters.

```dart
class MeterScanDraft {
  final String? meterNumber;
  final double? readingValue;
  final String confidence;
  final String? notes;
  final bool clientFound;
  final Map<String, dynamic>? client;
  final double? previousReading;
  final double? estimatedUsage;
  final List<String> warnings;

  MeterScanDraft.fromJson(Map<String, dynamic> j)
      : meterNumber = j['extracted']['meter_number'],
        readingValue = (j['extracted']['reading_value'] as num?)?.toDouble(),
        confidence = j['extracted']['confidence'],
        notes = j['extracted']['notes'],
        clientFound = j['match']['found'] ?? false,
        client = j['match']['client'],
        previousReading = (j['previous_reading'] as num?)?.toDouble(),
        estimatedUsage = (j['estimated_usage'] as num?)?.toDouble(),
        warnings = List<String>.from(j['warnings'] ?? const []);
}
```

---

## Notes for the backend team

- Model is `claude-haiku-4-5` by default, configurable via `CLAUDE_VISION_MODEL`
  in `.env` (e.g. switch to `claude-sonnet-4-6` for harder/analog meters).
- Cost ≈ $0.002 per scan on Haiku 4.5 (~$2 per 1,000 reads).
- Requires `ANTHROPIC_API_KEY` set in the backend `.env`; until then the endpoint
  returns `503` and the app should fall back to manual entry.

---

**Version**: 1.0 · **Last updated**: 2026-06-20 · Companion to
[FLUTTER_API_DOCUMENTATION.md](FLUTTER_API_DOCUMENTATION.md)
</content>
