# Counting API (v1)

For phone apps that count offerings (a native Android/iOS app, or a wrapper around the installable `/count`
screen). Deliberately narrow: it records gifts and counting sheets; **posting to the books is not available**
and stays on the website, where a second person approves.

Base URL: `https://<your-domain>/api/v1` · JSON in and out · send `Accept: application/json`.

## Signing in

```http
POST /login
{ "email": "counter@church.org", "password": "…", "device_name": "Tecno Spark 10", "code": "123456" }
```
- `code` is the 6-digit authenticator code (or a recovery code) and is required when the user has two-factor
  login. Without it the reply is `422 {"two_factor": true}`: ask for the code and send the request again.
- Users whose role requires two-factor login but who have not set it up get `403` until they do it on the website.
- Only users who may record gifts or count cash receive a token (`403` otherwise).
- 5 wrong attempts per minute per email + IP → `429`.
- Reply: `{ "token": "…", "expires_at": "…" }`. Tokens last **12 hours** (one Sunday). Send them as
  `Authorization: Bearer <token>`. Store the token in the platform's secure storage (Android Keystore / iOS
  Keychain), never in plain preferences.

`POST /logout` revokes the token immediately (do this when the phone is handed to someone else).

## Reading

| Request | Returns |
|---|---|
| `GET /me` | user, church, `can.capture` / `can.count`, active giving categories, channels, cash denominations (kobo) |
| `GET /batches` | open batches the user's branch may see (id, date, service, branch, total so far) |
| `GET /batches/{id}` | entries, totals, the counting sheet with who has signed, signatures required |

## Recording gifts (offline-safe)

```http
POST /batches/{id}/entries
{ "entries": [ { "client_uuid": "6f1c…", "income_category_id": 1, "channel": "cash", "amount": 5000 } ] }
```
- Generate a UUID **on the phone when the gift is recorded**, keep it with the gift, and resend the whole queue
  whenever the network returns. A gift whose `client_uuid` was already received is not recorded twice.
- `amount` is in naira (decimals allowed). Up to 500 entries per request.
- Each entry gets its own result: `synced` (with `id`) or `rejected` (with `message`). Keep rejected ones on
  screen for a person to fix; drop synced ones from the queue.

## Counting sheet

```http
POST /batches/{id}/count   { "denominations": { "100000": 7, "50000": 1 } }   → { "total": 750000 }
POST /batches/{id}/sign
```
- Keys are note/coin values in **kobo** from `GET /me` (`100000` = ₦1,000 note), values are how many.
- Saving a changed sheet clears earlier signatures. The required number of different counters must sign before
  the Financial Secretary can post the batch on the website.

## Errors

`401` signed out or token expired · `402` church account read-only (subscription lapsed) · `403` not allowed ·
`404` not found or another church's data · `422` validation or a church rule (message explains) · `429` slow down.

## Building the native app

- The `/count` screen in the web app is the reference design: category chips, cash/transfer/POS, keypad,
  queue, denomination steppers with live difference, sign button.
- Recommended stack: Flutter or React Native (Expo), with an encrypted local queue and the OS keystore for
  the token. Ship to Play Store first (most counters use Android); minimum Android 8.
- Test against a staging server with the demo church (`php artisan migrate:fresh --seed`).

# Devices and sync (v1, Phase 6A)

For the offline client (`/app/offline`, Phase 6B, D-053) and any native app that works without a connection. Decision D-052.
A **device** (one phone, tablet or laptop) registers once, waits until a branch admin approves it, and then
pushes the work it recorded offline and pulls what changed on the server.

## Registering a device

```http
POST /devices/register
{ "email": "...", "password": "...", "code": "123456", "device_uuid": "<uuid made on the device>",
  "device_name": "Tecno Spark 10", "platform": "Android 14" }
```
- Sign-in follows the same rules as `POST /login` (two-factor `code`, `403` when two-factor is required but
  not set up, `429` after 5 wrong attempts).
- Reply `201` with the device state (below). The device is `pending`: its token may call `GET /devices/me`
  but not sync (`403`) until approved. Registering the same `device_uuid` again (same user) returns the same device; `409` if it belongs to someone else or was revoked.
- **Approval**: the branch admin, under Setup > Devices (someone other than the device's own user).
  Headquarters sees every branch's devices under Directs > Devices in all branches.

## Device state

Returned by register, refresh and `GET /devices/me`:

| Field | Meaning |
|---|---|
| `device` | `uuid`, `code` (`DEV3`), `name`, `status` (`pending`, `approved`, `revoked`), `branch_id`, `last_seq` (the last operation the server applied), `last_cursor` |
| `access_token` | 12 hours. `Authorization: Bearer <token>` |
| `refresh_token` | Valid for the offline window, **works once**: every refresh returns a new pair |
| `permit` | Approved devices only. The signed offline permit (below) |
| `permit_public_key` | Base64 Ed25519 public key that checks the permit |
| `offline_window_days` | The church's offline window (default 30, set per church from 1 to 90) |
| `server_time` | For the device's clock check |
| `can` | `members`, `giving`: what the device's user may record, so screens offer only that. The server checks every operation again |

```http
POST /devices/refresh
{ "device_uuid": "...", "refresh_token": "..." }
```
`401` when the refresh token is wrong, used or expired (sign in again with register). **`410 {"wipe": true}`
when the device was revoked: delete every local record and key at once.**

## The offline permit

`<base64url(json)>.<base64url(Ed25519 signature)>`. The JSON holds `church`, `branch`, `device`, `code`,
`user`, `plan`, `issued_at`, `valid_until` and `last_sync_at`. The device checks the signature with
`permit_public_key` without a connection and stops recording once `valid_until` has passed, or if its clock
reads earlier than `last_sync_at` (clock turned back). A fresh permit comes with every refresh.

## Pushing work

```http
POST /sync/push
{ "batch_uuid": "<uuid>", "operations": [
  { "op_uuid": "<uuid>", "seq": 41, "type": "giving.batch.open",
    "data": { "client_uuid": "<uuid>", "provisional_number": "GB-DEV3-0012", "service_date": "2026-10-18", "service_name": "Sunday Service" } },
  { "op_uuid": "<uuid>", "seq": 42, "type": "giving.entry.add",
    "data": { "client_uuid": "<uuid>", "batch_client_uuid": "<the batch's client_uuid>", "income_category_id": 4,
              "channel": "cash", "amount": "5000", "member_client_uuid": "<optional>" } },
  { "op_uuid": "<uuid>", "seq": 43, "type": "member.upsert",
    "data": { "client_uuid": "<uuid>", "first_name": "Ngozi", "last_name": "Eze", "phone": "0803..." } }
] }
```
- `seq` is the device's own counter, starting at 1 with no gaps. The server applies operations **strictly in
  order**, one transaction each. Up to 200 operations per push.
- **Retries are safe.** An `op_uuid` already applied comes back as `duplicate` with its first result; nothing
  is recorded twice. Resend anything you have no reply for.
- **A gap** (the server expects 5, the push starts at 6) stops the push: `409` with `resume_from` (the `seq`
  to send next) and `last_seq`.
- Each result: `{ "op_uuid", "seq", "status": "applied" | "issue" | "duplicate", "result": {...} }`.
  - `applied`: `result` has the server `id` (and `member_no` and `version` for members). Map your
    `client_uuid` to it.
  - `issue`: the server refused the entry (closed period, switched-off account, unknown batch, not allowed,
    bank account missing for a transfer...). `result.reason` says why in plain words. The entry is kept in
    **Sync Issues** for the treasurer to fix and resubmit or withdraw. It is never dropped or changed. The
    sequence moves on, so later work is not held up.
- Giving batches keep the device's `provisional_number` beside the server's batch; the final, gap-free batch
  number is given when the batch is posted on the website, as before.
- **Members**: send `id` and `base_version` (from the last pull) to change an existing member. The last
  writer wins; if the server copy was newer, the details it replaced are kept in a conflict log. Needs
  `members.manage`.

### Operations (Phase 6C, D-054)

| `type` | `data` | Needs |
|---|---|---|
| `giving.batch.open` | `client_uuid`, `provisional_number`, `service_date`, `service_name`, `notes?` | `giving.capture` |
| `giving.entry.add` | `client_uuid`, `batch_id` or `batch_client_uuid`, `income_category_id`, `channel`, `amount`, `member_id` or `member_client_uuid`?, `reference?` | `giving.capture` |
| `giving.count.save` | `batch_id` or `batch_client_uuid`, `denominations` (kobo value → how many), `notes?` | `giving.count` |
| `giving.count.sign` | `batch_id` or `batch_client_uuid`, `total` (kobo, the total the user saw) | `giving.count` |
| `petty_cash.voucher` | `client_uuid`, `provisional_number`, `imprest_float_id`, `date`, `payee`, `account_id`, `fund_id`, `cost_centre_id?`, `description`, `amount` | `imprest.spend` |
| `requisition.raise` | `client_uuid`, `provisional_number`, `request_date`, `payee_name`, `vendor_id?`, `account_id`, `fund_id`, `cost_centre_id?`, `department?`, `description`, `amount`, `submit` | `expenses.request` |
| `cash.count` | `client_uuid`, `provisional_number`, `account_id`, `counted_on`, `denominations`, `notes?` | `cash.count` |
| `member.upsert` | see above | `members.manage` |

Vouchers, requisitions and cash counts return `number` (the branch's gap-free number) beside
`provisional_number`. Sending the same `client_uuid` again returns the first document.

## Pulling changes

`GET /sync/pull?cursor=0` returns a **snapshot**: accounts, funds, income categories, cost centres, vendors,
the branch's members and its open giving batches. `{ "cursor", "has_more", "snapshot": true, "upserts":
{ "<entity>": [rows] }, "deletes": {} }`.

Then `GET /sync/pull?cursor=<last cursor>` returns only what changed since, for the device's branch and the
church-wide lists: `upserts` and `deletes` (`{ "member": [ids] }`). Up to 1,000 changes per reply: while
`has_more` is true, pull again with the new cursor.

Every reply also has:
- `upserts.imprest_float`: the user's active floats with `balance` (kobo) now, sent whole each time;
- `issues`: this device's refused entries from the last 90 days, `{ op_uuid, status, note, number }`, where
  `status` is `open`, `resubmitted` (with the final `number`) or `withdrawn` (with the treasurer's `note`).

Add `&pending=N` (also on `GET /devices/me`): how many entries are still waiting on the device. The year-end
checklist waits until every device of the branch reports none.

## Sync errors

`401` token expired (refresh) · `402` church account read-only · `403` device not approved, or not a device
token · `409` sequence gap (`resume_from`) · `410` device revoked: wipe · `423` branch locked, closed or
merged: keep the work and try later, or ask Headquarters.
