# Bills API — Hướng dẫn chi tiết cho Frontend

Tài liệu này mô tả API `bills` (hóa đơn) để đội frontend hiểu cách gọi, cấu trúc dữ liệu, quy tắc validate, và logic nghiệp vụ liên quan.

**Base URL**: /api/v1/bills

---

## Tổng quan

- Mục đích: quản lý hóa đơn (tạo, liệt kê, xem chi tiết, cập nhật, xóa) gắn với `room` và `resident`.
- Yêu cầu chung: endpoint yêu cầu authentication; nhiều route yêu cầu quyền `admin` (middleware `requireAdmin`).
- Định dạng ngày:
  - `billing_period`: `YYYY-MM` (ví dụ `2026-05`)
  - `due_date`: `YYYY-MM-DD` (DATE)
  - Các thời gian trả về (`created_at`, `updated_at`) dùng ISO 8601 datetime string

---

## Các trường chính (tổng hợp)

- `id` (UUID): id của hóa đơn
- `code` (string): mã hóa đơn sinh tự động
- `room_id` (UUID): id phòng
- `resident_id` (UUID): id cư dân (người liên quan đến hóa đơn)
- `billing_period` (string): kỳ thu, format `YYYY-MM`
- `due_date` (string): hạn trả, `YYYY-MM-DD`
- `payment_method` (string|null)
- `notes` (string|null)
- `total_amount` (number|null)
- `status` (string): `draft` | `paid` | `pending` | `overdue`
- `created_by` (UUID|null)
- `created_at`, `updated_at`, `deleted_at` (datetime strings)

### bill_items
Mỗi `bill` có một mảng `bill_items` mô tả các khoản phí thuộc hóa đơn.
- Trường trả về (`BillItem`):
  - `id` (UUID)
  - `bill_id` (UUID)
  - `fee_name` (string)
  - `quantity` (integer)
  - `unit_price` (number)
  - `created_at` (datetime)
- Trường gửi khi tạo (`BillItemCreate`):
  - `fee_name`: required string
  - `quantity`: optional integer (default 1)
  - `unit_price`: required number >= 0

---

## Endpoint: POST /api/v1/bills — Tạo hóa đơn mới

- Authentication: có (middleware `verify`), thường yêu cầu `requireAdmin`.
- Body: `application/json` theo `BillCreateBody`.

### Request body (example)
```json
{
  "room_id": "466256c1-0cac-4784-af74-8a16b5576853",
  "resident_id": "77862ec4-3c2b-4e95-8c90-dce5226e76f1",
  "billing_period": "2026-05",
  "due_date": "2026-05-30",
  "payment_method": "cash",
  "notes": "Thanh toán tiền phòng tháng 5",
  "total_amount": 5000000,
  "status": "pending",
  "bill_items": [
    { "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000 },
    { "fee_name": "Điện", "quantity": 1, "unit_price": 500000 },
    { "fee_name": "Nước", "quantity": 1, "unit_price": 500000 }
  ]
}
```

### Validation rules
- `room_id`, `resident_id`: UUID required
- `billing_period`: regex `^\d{4}-\d{2}$` (YYYY-MM)
- `due_date`: string date (no strict format enforced in validation beyond being a string; API expects `YYYY-MM-DD`)
- `status`: one of `draft`, `paid`, `pending`, `overdue` (default `pending`)
- `bill_items`: non-empty array minimum 1 item; each item must follow `BillItemCreate` schema
  - `fee_name`: non-empty string, max 255
  - `quantity`: integer >= 1 (default 1)
  - `unit_price`: number >= 0

Nếu payload không hợp lệ, API trả `422` với hình thức lỗi:
```json
{
  "type": "VALIDATION_ERROR",
  "httpStatus": 422,
  "messages": { "vi": "Dữ liệu không hợp lệ", "en": "Invalid data" },
  "additionalData": { "errors": [ { "field": "bill_items.0.unit_price", "code": "INVALID_NUMBER_TYPE", "message": { ... } } ] }
}
```

### Business logic khi tạo
- Server sinh `code` tự động (`generateBillCode()`) nếu không truyền
- Tạo `bills` record trong transaction
- Nếu có `bill_items`, provider sẽ bulk-create các `bill_items` với `bill_id` gắn với `bill` vừa tạo
- `total_amount` nếu không truyền sẽ mặc định 0; tuy nhiên frontend nên tính tổng từ `bill_items` và gửi chính xác nếu muốn hiển thị ngay
- Ghi `created_by` từ `req.user.id` khi tạo
- Audit log sẽ được tạo (AuditLogService)

### Response
- 200 (hoặc success wrapper): dữ liệu `bill` đã tạo (kèm `bill_items` trả từ DB, có `id`, timestamps)
- Ví dụ:
```json
{
  "success": true,
  "data": {
    "id": "...",
    "code": "BILL-202605-0001",
    "room_id": "...",
    "resident_id": "...",
    "billing_period": "2026-05",
    "due_date": "2026-05-30",
    "total_amount": 5000000,
    "status": "pending",
    "bill_items": [ { "id": "...", "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000, "created_at": "..." }, ... ],
    "created_at": "...",
    "updated_at": "..."
  }
}
```

---

## Endpoint: GET /api/v1/bills — Danh sách (pagination, filters)

- Query params hỗ trợ bởi `QueryParamsSchema` (chung): `page`, `pageSize`, `sortField`, `sortOrder`, `filters`.
- Backend cung cấp `getAllOptimized` với join để trả `room_name`, `apartment_name`, `building_name`, `resident_full_name`, `resident_email`, `resident_phone`.
- Default pageSize: 20, max 100.

Ví dụ request:
```
GET /api/v1/bills?page=1&pageSize=20&sortField=created_at&sortOrder=desc
```

Response: { count, rows, page, pageSize }

Filter có thể truyền qua `filters` object (thực tế code áp dụng `...(options?.filters || {})` khi build `where`).

---

## Endpoint: GET /api/v1/bills/:id — Chi tiết hóa đơn

- Trả chi tiết bill kèm `bill_items`.
- Nếu không tìm thấy trả 404 (hoặc `null`) — tuỳ cách controller xử lý.

---

## Endpoint: PUT/PATCH /api/v1/bills/:id — Cập nhật

- Body theo `BillUpdateBody` schema (không cho cập nhật `bill_items` qua endpoint này; nếu cần update items, backend có provider/endpoint riêng hoặc cần triển khai).
- Validation tương tự `BillCreateBody` nhưng fields optional.
- Business rule: cập nhật `status` là supported. Nếu thay đổi `total_amount` frontend nên đồng bộ với `bill_items` service.

---

## Xử lý lỗi phổ biến
- 422 VALIDATION_ERROR: payload không hợp lệ (thường lỗi field types/format)
- 500 DATABASE / Sequelize errors: lỗi quan hệ/association—nếu gặp `SequelizeEagerLoadingError`, báo backend xem lại `init-models` associations
- 401/403: thiếu auth hoặc quyền
- 400 Bad JSON: body không phải JSON hợp lệ (ví dụ thiếu quote/comma) — kiểm tra payload JSON trước khi gửi

---

## Gợi ý UI / FE
- Khi tạo hóa đơn từ UI: tính `total_amount` client-side từ `bill_items` và gửi kèm để backend lưu, tránh mismatch
- Hiển thị trạng thái `pending`/`paid`/`overdue` rõ màu sắc
- Khi show form tạo: validate client-side trùng với server (billing_period format, due_date required, unit_price >= 0)
- Sau tạo: cập nhật danh sách bằng refetch `GET /api/v1/bills` hoặc realtime update nếu dùng websockets

---

## Các chú ý kỹ thuật cho FE-devs
- Authorization header: gửi token như bình thường (bearer) — backend sử dụng `verify` middleware.
- Request size: `bill_items` thường nhỏ; nếu tạo nhiều item > 100, cân nhắc batch API
- Định dạng JSON: ensure `due_date` là string `YYYY-MM-DD` khi gửi
- Nếu cần trường mở rộng (discount, tax), thỏa thuận trước với backend — hiện schema không support

---

## Ví dụ đầy đủ (curl)
```bash
curl -X 'POST' \
  'http://localhost:3001/api/v1/bills' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <TOKEN>' \
  -d '{
  "room_id": "466256c1-0cac-4784-af74-8a16b5576853",
  "resident_id": "77862ec4-3c2b-4e95-8c90-dce5226e76f1",
  "billing_period": "2026-05",
  "due_date": "2026-05-30",
  "payment_method": "cash",
  "notes": "Thanh toán tiền phòng tháng 5",
  "total_amount": 5000000,
  "status": "pending",
  "bill_items": [
    { "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000 },
    { "fee_name": "Điện", "quantity": 1, "unit_price": 500000 },
    { "fee_name": "Nước", "quantity": 1, "unit_price": 500000 }
  ]
}'
```

---

Nếu bạn muốn mình thêm:
- tệp OpenAPI/Swagger đoạn cho `bills` (examples + response schemas),
- hoặc tạo một mini-PR mẫu ở frontend (fetch helpers) để gọi endpoint này,
hãy nói mình biết ưu tiên nào nhé.