# DYU Vendor Payment Tracking — Mobile API (v1)

> Server: CodeIgniter 4 · Auth: Bearer JWT (HS256) · Default base URL: `http://<host>/api/v1`

**Browser-friendly HTML version:** open [`docs/MOBILE_API.html`](MOBILE_API.html) in any browser, or visit `http://localhost:8080/docs/api` while the dev server is running.

**Account-wise flow (app integration):** see [`APP_INTEGRATION_ACCOUNT_FLOW.md`](APP_INTEGRATION_ACCOUNT_FLOW.md) for breaking changes, UI checklist, and E2E examples.

This document is for the Flutter mobile app team. Everything the Account Incharge and Approver flows need lives under `/api/v1`. Endpoints under that prefix return JSON only and are independent of the admin panel sessions.

---

## 1. Conventions

### 1.1 Base URL

```
http://<your-server>/api/v1
```

The app is built on CodeIgniter 4. In development the base URL is `http://localhost:8080/api/v1`. Use the value provided by the backend for staging/production.

### 1.2 Standard envelope

Every response uses the same envelope:

```json
{
  "status":  "success",         // "success" | "error"
  "message": "OK",              // human-readable
  "data":    { ... } | [ ... ], // present on success
  "errors":  { ... } | null     // present on validation errors (status=error, http=422)
}
```

Always check the **HTTP status code first**, then `status`, then `data`.

### 1.3 HTTP status codes

| Code  | Meaning                                                    |
| ----- | ---------------------------------------------------------- |
| `200` | Success                                                    |
| `201` | Created (returned by POST endpoints that create something) |
| `400` | Bad request (logical error)                                |
| `401` | Authentication required / invalid or expired token         |
| `403` | Authenticated but not authorised for this resource         |
| `404` | Resource not found                                         |
| `409` | Conflict (e.g. trying to edit a submitted request)         |
| `422` | Validation failed — see `errors` map                       |
| `500` | Server error — please report                               |

### 1.4 Authentication header

Every protected endpoint requires:

```
Authorization: Bearer <jwt token>
Content-Type: application/json     (only for JSON bodies)
```

Tokens are issued by `POST /auth/login`. They are **HS256 JWT** signed with a server-side secret and expire after **30 days** by default (configurable).

### 1.5 Date and currency formats

| Type      | Format                | Example               |
| --------- | --------------------- | --------------------- |
| Date      | `YYYY-MM-DD`          | `2026-05-23`          |
| Date+Time | `YYYY-MM-DD HH:MM:SS` | `2026-05-23 16:42:11` |
| Currency  | Decimal (INR)         | `117500.00`           |

### 1.6 Pagination

List endpoints accept `page` (default 1) and `per_page` (default 25, max 100) and return:

```json
"data": {
  "rows":        [ ... ],
  "total":       147,
  "page":        2,
  "per_page":    25,
  "total_pages": 6
}
```

### 1.7 Common error examples

```http
HTTP/1.1 401 Unauthorized
{ "status": "error", "message": "Token has expired.", "errors": null }
```

```http
HTTP/1.1 422 Unprocessable Entity
{
  "status": "error",
  "message": "Validation failed.",
  "errors": {
    "project_id": "Project is required.",
    "invoice_amount": "Invoice amount must be greater than zero."
  }
}
```

---

## 2. Domain model — high level

```
User (App user)
  ├─ Account Incharge  → Project → Account (Diesel, Labour, …)
  │                         └─ raises Payment Requests under that account
  └─ Approver          → Account Approval Levels (L1…Ln, multiple users per level)
                            └─ any one approver completes the level (peers SKIPPED)

Payment Request
  ├─ account_id (required)
  ├─ Attachments (incharge)
  ├─ Approvals (one row per assignee per level)
  ├─ Approval logs / approval attachments
  └─ Transactions (disbursements)
```

### Payment Request states

```
DRAFT  ─submit→  UNDER_APPROVAL  ─approve→ … ─approve final→ APPROVED
                                ─reject→  REJECTED
                                ─return→  AMENDMENT_REQUIRED ──submit→ UNDER_APPROVAL
APPROVED  ─process→  PAYMENT_PROCESSING / PARTIALLY_PAID / PAID
*  ─cancel→ CANCELLED   (allowed before PAYMENT_PROCESSING)
```

**Editable**: `DRAFT`, `AMENDMENT_REQUIRED` (only by the requester).

---

## 3. Endpoint reference

### 3.1 Auth & Session

#### `POST /auth/login`  (public)

Authenticate the user and return a Bearer token. Optionally register the device for push notifications in the same call.

Request:

```json
{
  "email":    "chougule.koustubh@gmail.com",
  "password": "admin@123",
  "device": {
    "uuid":         "abc-123",
    "platform":     "ANDROID",          // ANDROID | IOS | WEB
    "fcm_token":    "fcmTokenString",
    "app_version":  "1.0.0",
    "device_model": "Pixel 8"
  }
}
```

Response `200`:

```json
{
  "status": "success",
  "message": "Login successful.",
  "data": {
    "token":      "<jwt>",
    "token_type": "Bearer",
    "expires_in": 2592000,
    "user": {
      "id": 3, "user_code": null, "full_name": "Koustubh Chougule",
      "email": "chougule.koustubh@gmail.com", "mobile": "...",
      "user_type": "WEB_ADMIN", "designation": "Senior Project Incharge",
      "department": null, "profile_image_path": null,
      "profile_image_url": null, "is_active": 1, "last_login_at": "2026-05-23 16:30:00",
      "roles": [ { "id": 3, "role_name": "Project Incharge", "role_key": "PROJECT_INCHARGE", "role_scope": "APP" } ]
    }
  }
}
```

Errors: `401` invalid credentials · `403` inactive account · `422` missing fields.

#### `POST /auth/logout`  (auth)

Body (optional):

```json
{ "fcm_token": "fcmTokenString" }
```

If `fcm_token` is provided, only that device is deactivated; otherwise all devices for the user are deactivated. Tokens are stateless so logout is essentially a courtesy + device cleanup; the app should also discard the token locally.

#### `GET /auth/me`  (auth)

Returns the same `user` payload as login.

#### `GET /health`  (public)

Quick connectivity probe — useful for the splash screen.

```json
{ "status": "success", "message": "Mobile API is reachable.",
  "data": { "time": "...", "app": "DYU Vendor Payment Tracking — Mobile API", "api_version": "v1" } }
```

---

### 3.2 Profile

#### `GET /profile`

Same shape as `/auth/me`.

#### `PUT /profile`  *(also accepts `POST /profile`)*

Body (any subset):

```json
{ "full_name": "...", "mobile": "...", "designation": "...", "department": "..." }
```

#### `POST /profile/change-password`

```json
{ "current_password": "...", "new_password": "min 6 chars" }
```

Returns `401` when the current password is wrong, `422` on validation failure.

#### `POST /profile/avatar`  *(multipart/form-data)*

Field: `avatar` — JPG/PNG/WEBP up to 5 MB. Response includes `profile_image_path` and `profile_image_url`.

---

### 3.3 Devices (Firebase / FCM)

#### `POST /devices/register`

```json
{
  "uuid":         "abc-123",
  "platform":     "ANDROID",
  "fcm_token":    "fcmTokenString",
  "app_version":  "1.0.0",
  "device_model": "Pixel 8"
}
```

**Idempotent** — calling with the same `fcm_token` updates the existing row. Re-call this whenever Firebase rotates the token.

#### `POST /devices/unregister`

```json
{ "fcm_token": "fcmTokenString" }
```

---

### 3.4 Vendors  *(read-only)*

#### `GET /vendors`

Query: `search`, `vendor_type` (`VENDOR|SUPPLIER|CONTRACTOR|CONSULTANT|OTHER`), `page`, `per_page`.

#### `GET /vendors/{id}`

Returns the full vendor record (without timestamps the app doesn't need).

---

### 3.5 Projects  *(scoped to current user)*

The list contains only projects where the user is **either** an active **account incharge** **or** an active **approver** on at least one account under the project.

#### `GET /projects`

Query:

| Param      | Description                                                  |
| ---------- | ------------------------------------------------------------ |
| `role`     | `incharge` \| `approver` \| `all` (default)                  |
| `status`   | `PLANNED \| ACTIVE \| ON_HOLD \| COMPLETED \| CANCELLED`     |
| `search`   | Matches name / code / location                               |
| `page`     | Page number                                                  |
| `per_page` | Page size                                                    |

Each row is annotated:

```json
{
  "id": 2, "project_code": "PRJ-001", "project_name": "Rajaji Nagar Apartment Project",
  "location": "Bengaluru", "project_status": "ACTIVE",
  "is_incharge": true, "incharge_is_primary": true,
  "is_approver": true, "my_levels": [1, 2],
  "my_account_ids": [1, 3],
  "account_count": 4
}
```

#### `GET /projects/{id}`

Returns the project plus nested **accounts**. Each account includes incharges and approval levels with **multiple approvers**. 403 if the user has no account membership on this project.

```json
{
  "data": {
    "id": 2, "project_name": "...",
    "is_incharge": true, "is_approver": true,
    "my_levels": [1], "my_account_ids": [1],
    "accounts": [
      {
        "id": 1, "account_code": "DIESEL", "account_name": "Diesel",
        "i_am_incharge": true, "i_am_approver": false, "can_raise": true,
        "incharges": [
          { "user_id": 5, "full_name": "Project Incharge", "is_primary": true, "is_me": true }
        ],
        "approval_levels": [
          {
            "level_number": 1, "level_name": "L1 Review",
            "approvers": [
              { "user_id": 4, "full_name": "Level 1 Approver", "is_me": false },
              { "user_id": 6, "full_name": "koustubh", "is_me": false }
            ],
            "is_my_level": false
          },
          {
            "level_number": 2, "level_name": "L2 Final",
            "approvers": [
              { "user_id": 3, "full_name": "Koustubh Chougule", "is_me": false }
            ],
            "is_my_level": false
          }
        ]
      }
    ],
    "request_counts": { "DRAFT": 4, "UNDER_APPROVAL": 2, "PAID": 1 },
    "total_requests": 7
  }
}
```

Use `accounts[].can_raise === true` to build the create-request account picker for incharges.

---

### 3.6 Payment Requests

#### `GET /payment-requests`

Query:

| Param        | Description                                                      |
| ------------ | ---------------------------------------------------------------- |
| `scope`      | `mine` (default) / `to_approve` / `all`                          |
| `status`     | one of the request states                                        |
| `project_id` | filter to one project                                            |
| `account_id` | filter to one account                                            |
| `vendor_id`  | filter to one vendor                                             |
| `from`,`to`  | `YYYY-MM-DD` filter on created_at                                |
| `search`     | request_no / invoice_no / project / account / vendor name        |
| `page`,`per_page` |                                                             |

Returns paginated list of compact request rows (includes `account_id`, `account_name`, `account_code`).

#### `GET /payment-requests/{id}`

Full bundle:

```json
{
  "data": {
    "id": 4, "request_no": "PR-20260523-00004-7",
    "project_id": 2, "account_id": 1, "vendor_id": 2,
    "project_name": "...", "account_name": "Diesel", "account_code": "DIESEL",
    "vendor_name": "...",
    "invoice_no": "INV-AC-001", "invoice_date": "2026-05-20",
    "invoice_amount": "100000.00",
    "ra_bill_number": 3,
    "status": "UNDER_APPROVAL", "current_level": 2, "max_level": 2,
    "submitted_at": "...", "approved_at": null,
    "attachments":  [ { "id":..., "url":"http://...", "attachment_type":"INVOICE", ... } ],
    "status_logs":  [ { "previous_status":"DRAFT", "new_status":"UNDER_APPROVAL", "action_by_name":"...", "remarks":"..." } ],
    "approvals":    [ { "level_number":1, "status":"APPROVED", "approver_name":"...", "attachments": [ { "id": 1, "original_file_name": "signed-checklist.pdf", "url": "http://..." } ] } ],
    "approval_logs":[ { "action":"APPROVED", "level_number":1, "approver_name":"...", "remarks":"...", "attachments": [ ... ] } ],
    "approval_attachments": [ { "id": 1, "level_number": 1, "action": "APPROVED", "original_file_name": "...", "url": "http://..." } ],
    "transactions": [ { "transaction_no":"PAY-20260523-...", "payment_amount":"117000.00", "payment_status":"SUCCESS", "processed_by_name":"..." } ],
    "paid_amount":   117000,
    "balance_amount": 0
  }
}
```

> **Note:** `approvals[]` may contain **multiple rows for the same `level_number`** (one per assignee). Statuses include `PENDING`, `APPROVED`, `REJECTED`, `SKIPPED`, `RETURNED_FOR_AMENDMENT`.

> **`ra_bill_number`:** Plain integer serial unique per **project + vendor**. Assigned automatically on the **first successful submit**; `null` on drafts; never changes on amendment re-submit.

#### `POST /payment-requests`  *(Account Incharge only)*

```json
{
  "project_id": 2,
  "account_id": 1,
  "vendor_id":  2,
  "invoice_no": "INV-AC-001",
  "invoice_date": "2026-05-20",
  "invoice_amount": 100000,
  "action": "draft"
}
```

* `account_id` is **required**. It must belong to `project_id`.
* If `action: "submit"`, the request is created **and** moved straight to `UNDER_APPROVAL` (level 1). On that first submit the server also assigns `ra_bill_number` (next integer for this project + vendor).
* `request_no` is generated server-side.
* **403** if the caller is not an active incharge of the chosen **account**.
* **422** if `account_id` is missing or does not belong to the project.

#### `PUT /payment-requests/{id}`  *(also `POST /payment-requests/{id}`)*

Partial update (any subset of header fields including `account_id`). Allowed only while `status` is `DRAFT` or `AMENDMENT_REQUIRED`. Caller must be the original requester.

#### `DELETE /payment-requests/{id}`

Soft-deletes a `DRAFT` request. Once submitted use `cancel`.

#### `POST /payment-requests/{id}/submit`

Body (optional): `{ "remarks": "..." }`. Moves `DRAFT`/`AMENDMENT_REQUIRED` → `UNDER_APPROVAL`, seeds **one approval row per assignee per account level**, and notifies **all** level-1 approvers. Assigns `ra_bill_number` on first submit only (unchanged on amendment re-submit).

#### `POST /payment-requests/{id}/cancel`

Body (optional): `{ "remarks": "..." }`. Allowed in `DRAFT`, `SUBMITTED`, `UNDER_APPROVAL`, `AMENDMENT_REQUIRED`, `REJECTED`. Sets status to `CANCELLED`.

---

### 3.8 Attachments

Only modifiable while the request is `DRAFT` or `AMENDMENT_REQUIRED` and only by the requester.

#### `GET /payment-requests/{id}/attachments`

Returns active attachments for the request. Each row contains:

```json
{
  "id": 1, "attachment_type": "INVOICE",
  "file_name": "1779534358-73301fbc.txt", "original_file_name": "invoice.pdf",
  "file_path": "uploads/payment-requests/4/1779534358-73301fbc.txt",
  "file_url":  "http://localhost:8080/uploads/payment-requests/4/...",
  "url":       "http://localhost:8080/uploads/payment-requests/4/...",
  "mime_type": "application/pdf", "file_size_bytes": 23440,
  "uploaded_from": "APP", "is_active": 1, "created_at": "..."
}
```

#### `POST /payment-requests/{id}/attachments`  *(multipart/form-data)*

| Field             | Required | Notes                                                                                    |
| ----------------- | -------- | ---------------------------------------------------------------------------------------- |
| `file`            | yes      | `jpg, jpeg, png, webp, gif, pdf, doc, docx, xls, xlsx, txt`. Max 25 MB.                  |
| `attachment_type` | no       | `INVOICE` (default `OTHER`) — `INVOICE \| WORK_PHOTO \| WORK_ORDER \| MEASUREMENT_SHEET \| APPROVAL_DOC \| OTHER` |

#### `DELETE /payment-requests/{id}/attachments/{aid}`

Soft-deletes (marks `is_active = 0`). The file remains on disk for audit.

---

### 3.9 Approval workflow

#### `GET /approvals/inbox`

Requests where I am a **current** assignee (`is_current=1`, `PENDING`). Each row carries `my_level`, `assigned_at`, `account_id`, `account_name`, `account_code`.

Query: `search`, `project_id`, `account_id`, `page`, `per_page`.

#### `GET /approvals/history`

Requests I have already acted on (any of `APPROVED`, `REJECTED`, `RETURNED_FOR_AMENDMENT`). Includes account fields.

#### `POST /payment-requests/{id}/approve`

Supports **JSON** or **multipart/form-data**.

| Field | Required | Notes |
| ----- | -------- | ----- |
| `remarks` | no | Approval comment |
| `file` | no | Optional supporting document (jpg, png, pdf, doc, xls, txt — max 25 MB) |
| `attachment_remarks` | no | Caption for the uploaded file |

JSON example:

```json
{ "remarks": "All good at L1" }
```

Behaviour (**any-one approval**):

* Marks **your** assignee row `APPROVED`.
* Peer assignees at the same level are marked `SKIPPED` (`AUTO_SKIPPED` in logs) and can no longer act.
* If a higher level exists → all assignees at the next level become current and are notified.
* If this is the **final level** → request status becomes `APPROVED`, requester is notified.
* Optional file is stored against this approval action and returned in `data.attachment` when present.

Response (compact):

```json
{
  "status": "success",
  "message": "Approved and forwarded to next level.",
  "data": {
    "id": 4,
    "status": "UNDER_APPROVAL",
    "current_level": 2,
    "max_level": 2,
    "attachment": {
      "id": 12,
      "approval_log_id": 45,
      "level_number": 1,
      "action": "APPROVED",
      "original_file_name": "l1-signoff.pdf",
      "url": "http://localhost:8080/uploads/payment-requests/4/approvals/45/..."
    }
  }
}
```

#### `POST /payment-requests/{id}/reject`

Supports **JSON** or **multipart/form-data**.

| Field | Required | Notes |
| ----- | -------- | ----- |
| `remarks` | **yes** | Rejection reason |
| `file` | no | Optional supporting document |
| `attachment_remarks` | no | Caption for the file |

```json
{ "remarks": "Duplicate invoice" }
```

Sets request status to `REJECTED`, notifies the requester. Optional attachment is linked to the rejection action at the current level.

#### `POST /payment-requests/{id}/return`

Supports **JSON** or **multipart/form-data**.

| Field | Required | Notes |
| ----- | -------- | ----- |
| `remarks` | **yes** | What the incharge must fix |
| `file` | no | Optional annotated doc / checklist |
| `attachment_remarks` | no | Caption for the file |

```json
{ "remarks": "Please attach work order" }
```

Sets request status to `AMENDMENT_REQUIRED`, resets all approval rows so resubmission re-issues from level 1, notifies the requester. Optional attachment is stored for audit.

**Authorization**: 403 if you are not the current-level approver. 409 if the request is not `UNDER_APPROVAL`.

> **Note:** Approval attachments are separate from incharge request attachments (`/payment-requests/{id}/attachments`). They appear on `approval_logs[].attachments`, `approvals[].attachments`, and the flat `approval_attachments` array in the request bundle.

---

### 3.10 Notifications

Notifications are written to `notifications` and pushed via FCM to every active device registered for the recipient. Delivery status is tracked in `notification_deliveries`.

| Method | Path                                | Description                  |
| ------ | ----------------------------------- | ---------------------------- |
| GET    | `/notifications`                    | Paginated inbox              |
| GET    | `/notifications/unread-count`       | Just the badge count         |
| POST   | `/notifications/{id}/read`          | Mark one as read             |
| POST   | `/notifications/read-all`           | Mark all as read             |

`GET /notifications` query:

* `only_unread=1` to filter
* `page`, `per_page`

Each row:

```json
{
  "id": 12, "user_id": 3, "payment_request_id": 4,
  "title": "Payment processed in full",
  "message": "Request PR-20260523-00004-7: ₹ 117,000.00 via NEFT (Txn PAY-...)",
  "notification_type": "PAYMENT_PROCESSED",
  "data": { "transaction_no": "PAY-...", "payment_amount": 117000, "new_status": "PAID" },
  "is_read": 0, "read_at": null,
  "created_at": "2026-05-23 17:01:11"
}
```

Notification types currently emitted by the server:

| Type                   | When                                                                  |
| ---------------------- | --------------------------------------------------------------------- |
| `APPROVAL_PENDING`     | A request just landed in your inbox (you are the current approver)    |
| `REQUEST_APPROVED`     | Final approval cleared (notifies the requester)                       |
| `REQUEST_REJECTED`     | A request you raised was rejected                                     |
| `AMENDMENT_REQUIRED`   | A request you raised was returned for amendment                       |
| `PAYMENT_PROCESSED`    | The accounts team processed (full or partial) payment                 |
| `GENERAL`              | Used for "level X approved, moved to level Y" progress nudges         |

---

### 3.11 Dashboard

#### `GET /dashboard`

A single, user-aware summary that powers the home screen. Works equally well for incharges and approvers — fields that don't apply will simply be empty.

```json
{
  "data": {
    "requested_by_me": {
      "total": 14,
      "by_status": { "DRAFT": 3, "UNDER_APPROVAL": 4, "APPROVED": 2, "PAID": 5 },
      "recent": [
        { "id": 4, "request_no": "PR-...", "status": "UNDER_APPROVAL",
          "current_level": 2, "max_level": 2,
          "invoice_amount": "100000.00",
          "project_name": "Rajaji Nagar Apartment Project",
          "vendor_name": "Acme Construction Pvt Ltd",
          "created_at": "..." }
      ],
      "total_disbursed_inr": 542300.00
    },
    "inbox": {
      "pending": 3,
      "teaser": [ { "id": 4, "request_no": "...", "invoice_amount": "...",
                    "project_name": "...",
                    "vendor_name": "...", "my_level": 1, "assigned_at": "..." } ]
    },
    "projects":      { "count": 2 },
    "notifications": { "unread": 5 }
  }
}
```

---

## 4. End-to-end flow examples

### 4.1 Account Incharge: raise + submit a request

```
1. POST   /auth/login                          → token
2. GET    /vendors                             → pick vendor_id
3. GET    /projects?role=incharge              → pick project_id (where is_incharge=true)
4. GET    /projects/{id}                       → pick account_id where can_raise=true
5. POST   /payment-requests                    → project_id + account_id + vendor + invoice, action="draft"
6. POST   /payment-requests/{id}/attachments   → file=@invoice.pdf, attachment_type=INVOICE
7. POST   /payment-requests/{id}/submit        → UNDER_APPROVAL; all L1 approvers notified
```

### 4.2 Approver: clear inbox

```
1. GET    /auth/login                          → token
2. GET    /approvals/inbox                     → list of pending decisions
3. GET    /payment-requests/{id}               → review full bundle (attachments included)
4a. POST  /payment-requests/{id}/approve       → with remarks, advances to next level / final
4b. POST  /payment-requests/{id}/reject        → with mandatory remarks (final)
4c. POST  /payment-requests/{id}/return        → with mandatory remarks (back to incharge)
```

### 4.3 Incharge: respond to amendment

```
1. GET    /payment-requests?status=AMENDMENT_REQUIRED
2. GET    /payment-requests/{id}               → see the rejection remark in approval_logs[…].remarks
3. PUT    /payment-requests/{id}               → fix header fields
   POST   /payment-requests/{id}/attachments   → attach the missing doc
4. POST   /payment-requests/{id}/submit        → re-issues from level 1 again
```

### 4.4 Token refresh

The current implementation uses long-lived tokens (30 days). If a request returns `401` with `"Token has expired."`, force the user back to the login screen. A dedicated refresh-token endpoint can be added later if needed.

---

## 5. Server-side notes for the mobile dev (just FYI)

### 5.1 CORS

A permissive CORS filter is applied to `/api/*` so that browser-based debug tools (Postman web, Swagger UI, curl-from-browser) work without configuration.

### 5.2 CSRF

CSRF protection is **not** applied to the `/api/v1` group — the mobile app does not need to send a CSRF token. Authentication is purely Bearer.

### 5.3 File uploads

* Avatars: `POST /profile/avatar` field name `avatar`, max 5 MB.
* Attachments: `POST /payment-requests/{id}/attachments` field name `file`, max 25 MB.
* Server stores files under `public/uploads/` and returns absolute `file_url` / `url`.

### 5.4 Push notifications

FCM HTTP v1 dispatch is wired through `NotificationService` + `FcmClient`. Configure in `.env`:

```
firebase.enabled = true
firebase.credentialsPath = vendorapp-97434-firebase-adminsdk-fbsvc-8b55d1521a.json
```

`credentialsPath` may be absolute or relative to the project root. **Do not commit the JSON key file** — it is gitignored.

When a notification is created the server:

1. Persists it in `notifications` (in-app inbox).
2. Queues one row per active device in `notification_deliveries`.
3. Sends the FCM push immediately (best-effort).

Retry stuck rows manually or via cron:

```bash
php spark push:flush --limit 100
```

Mobile apps must register FCM tokens via `POST /devices/register` (or inline on login). Invalid / expired tokens are auto-deactivated.

### 5.5 Configuration knobs

`.env` exposes the following API knobs (server side):

```
api.jwtSecret      = '...'        # rotate this in production
api.jwtIssuer      = 'dyu-vpts'
api.jwtTtlMinutes  = 43200        # 30 days
api.uploadBasePath = 'uploads'
firebase.enabled   = true
firebase.credentialsPath = 'vendorapp-97434-firebase-adminsdk-fbsvc-8b55d1521a.json'
```

### 5.6 Test credentials (dev only)

| Email                            | Password   | Role on the seeded data                      |
| -------------------------------- | ---------- | -------------------------------------------- |
| `chougule.koustubh@gmail.com`    | `admin@123`| Project Incharge + L1 Approver of project #2 |
| `admin@dyu.com`                  | `admin@123`| L2 (final) Approver of project #2            |

---

## 6. Quick cURL smoke

```bash
# 1. Login
TOKEN=$(curl -sS -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"chougule.koustubh@gmail.com","password":"admin@123"}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")

# 2. Me
curl -sS http://localhost:8080/api/v1/auth/me \
  -H "Authorization: Bearer $TOKEN" | jq

# 3. List my projects
curl -sS "http://localhost:8080/api/v1/projects?role=all" \
  -H "Authorization: Bearer $TOKEN" | jq

# 4. Create + submit a request in one go (account_id required)
curl -sS -X POST http://localhost:8080/api/v1/payment-requests \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id":2,"account_id":1,"vendor_id":2,"invoice_no":"INV-001",
    "invoice_amount":50000,
    "action":"submit"
  }' | jq

# 5. Inbox of an approver
curl -sS http://localhost:8080/api/v1/approvals/inbox \
  -H "Authorization: Bearer $TOKEN" | jq
```

---

## 7. Versioning

* Current version: **v1** (under `/api/v1`).
* Backwards-incompatible changes will land under `/api/v2` with at least one minor release of overlap.
* Adding fields to existing responses is **not** considered a breaking change — please ignore unknown fields on the client.

---

## 8. Change log

| Date        | Change                                                  |
| ----------- | ------------------------------------------------------- |
| 2026-05-23  | Initial v1 release: auth, profile, devices, vendors, projects, payment requests + attachments, approvals, notifications, dashboard. |
| 2026-06-23  | Simplified payment request create/update: removed amount breakdown, due date, priority, work summary, remarks, and work-details endpoints. |
| 2026-06-23  | FCM HTTP v1 push dispatch wired (`FcmClient`, `firebase.*` env, `php spark push:flush`). |
| 2026-07-08  | Approvers may upload an optional attachment on approve / reject / return (`payment_request_approval_attachments`). |
| 2026-08-03  | Account-wise approval: `account_id` required on create; nested project accounts; multi-approver levels (any-one); peers `SKIPPED`. See `docs/APP_INTEGRATION_ACCOUNT_FLOW.md`. |
| 2026-08-27  | Added `ra_bill_number` — auto-assigned on first submit; unique per project + vendor; immutable thereafter. |

