# REST API Reference

Base path: `http://localhost:4000/api/v1`. All JSON request bodies use `Content-Type: application/json`; all routes except `/auth/login` require `Authorization: Bearer <JWT>`. Numeric primary IDs are generated by MySQL. No self-registration endpoint is exposed.

## Authentication

| Method | Path | Role | Function |
|---|---|---|---|
| POST | `/auth/login` | Public | Username/email and password → accessToken |
| GET | `/auth/me` | Logged-in | Own identity and role |
| POST | `/auth/change-password` | Logged-in | Current + new password, invalidates old tokens |
| GET | `/users` | Admin | User account list (no hashes) |
| POST | `/users` | Admin | Create role-bound login |
| PATCH | `/users/:id/status` | Admin | active/suspended/disabled |
| POST | `/users/:id/reset-password` | Admin | Reset password |

Login:
```bash
curl -X POST http://localhost:4000/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"YourStrongPassword#123"}'
```

Create employee with login:
```json
{
  "employee_no":"0042", "full_name":"Example Employee", "job_title":"Associate Software Engineer",
  "work_email":"example.employee@example.com", "join_date":"2026-11-01", "epf_base_salary":"40000.00",
  "login":{"username":"employee42", "email":"example.employee@example.com", "password":"UseDifferent#Password123"}
}
```

## Employees

| Method | Path | Role | Notes |
|---|---|---|---|
| GET / POST | `/employees` | Admin | Directory / create with optional login |
| GET / PATCH | `/employees/:employeeId` | Admin | Full detail, salary/leave history / edit |
| PATCH | `/employees/:employeeId/status` | Admin | active, inactive, terminated |
| DELETE | `/employees/:employeeId` | Admin | Only when no retained HR history |
| GET | `/employees/me` | Employee | Own employment profile only |
| PATCH | `/employees/me/contact` | Employee | Own personal email |
| GET / PUT | `/employees/me/bank-details` | Employee | Own masked bank details / add encrypted account |

Account number is never returned in plaintext by API; only last four digits. Use a secure private device and HTTPS in production.

## Leave requests

| Method | Path | Role |
|---|---|---|
| POST | `/leaves` | Employee |
| GET | `/leaves/mine?from=YYYY-MM-DD&to=YYYY-MM-DD` | Employee |
| POST | `/leaves/:id/cancel` | Employee, pending only |
| GET | `/leaves?status=pending` | Admin |
| GET | `/leaves/:id/decisions` | Admin |
| POST | `/leaves/:id/decision` | Admin |

Employee submit:
```json
{"leave_type":"annual","start_date":"2026-12-10","end_date":"2026-12-11","reason":"Personal leave"}
```
Admin rejection, reason mandatory:
```json
{"decision":"rejected","rejection_reason":"Insufficient leave coverage for those dates"}
```
Decisions can be changed by posting a subsequent decision; previous entries remain recorded. Email records are queued, and only delivered when SMTP and the email worker are configured.

## Financial periods / catalog

| Method | Path | Role |
|---|---|---|
| GET / POST | `/periods` | Admin + shareholder (GET), Admin (POST) |
| PATCH | `/periods/:id/status` | Admin |
| GET / POST | `/catalog/clients` | Admin |
| GET / POST | `/catalog/projects` | Admin |
| GET / POST | `/catalog/expense-categories` | Admin |

Create month: `POST /periods` → `{ "year":2026,"month":11 }`.

## Income and expenses

| Method | Path | Role |
|---|---|---|
| GET / POST | `/finance/income` | Admin |
| GET / POST | `/finance/expenses` | Admin |

Received income example (bank account and currency must match):
```json
{
  "period_id":1, "project_id":2, "bank_account_id":1,
  "description":"Website project payment", "order_date":"2026-10-10",
  "received_date":"2026-11-14", "gross_amount":"100000.00",
  "processing_fees":"0.00", "received_amount":"100000.00",
  "currency":"LKR", "payment_status":"received"
}
```
The linked period must match **received_date** for collected income or **order_date** for unpaid income. A payment produces exactly one bank credit entry. A pending order doesn't change cash.

Expense example:
```json
{
  "period_id":1,"category_id":1,"bank_account_id":1,
  "description":"AI software subscription","expense_date":"2026-11-14",
  "amount":"6886.00","currency":"LKR","status":"paid"
}
```
Use `"other_payment_source":"Cash paid by Director"` instead of `bank_account_id` for an unlinked payment source. Such expenses do **not** automatically alter a bank account's ledger.

## Bank accounts

| Method | Path | Role |
|---|---|---|
| GET / POST | `/accounts` | Admin |
| GET | `/accounts/:id/transactions` | Admin |
| POST | `/accounts/:id/adjustments` | Admin |
| POST | `/accounts/transfer` | Admin |
| GET | `/accounts/snapshots/all` | Admin / Shareholder |
| PUT | `/accounts/:id/snapshots/:periodId` | Admin |

Create account: `{ "account_name":"Company Current", "account_type":"bank", "currency":"LKR", "opening_balance":"50000.00" }`.

Transfer: `{ "from_account_id":1,"to_account_id":2,"date":"2026-11-14","amount":"1000.00","reference":"TRANSFER-NOV" }` (same currency only).

## Payroll and payslips

| Method | Path | Role |
|---|---|---|
| POST / GET | `/payroll/runs` | Admin |
| POST / GET | `/payroll/runs/:id/items` | Admin |
| PATCH | `/payroll/runs/:id/status` | Admin |
| POST | `/payroll/runs/:id/issue-payslips` | Admin |
| GET | `/payroll/my-payslips` | Employee |
| GET | `/payroll/payslips/:id/pdf` | Own employee / Admin |

Create run: `{ "period_id":1,"payment_date":"2026-11-30" }`.

Add employee salary to draft run:
```json
{
  "employee_id":4, "basic_pay":"50000.00", "skilled_pay":"40000.00",
  "allowances":"0", "bonus":"0", "epf_applicable_base":"50000.00",
  "other_deductions":"0"
}
```
Result: Gross **90,000**, Employee EPF **4,000**, Net **86,000**, employer EPF **6,000**, employer ETF **1,500**, employer total **97,500** (LKR). State transitions: draft → approved → paid; only draft can be voided. Issue payslips after approving.

## Shareholders and reporting

| Method | Path | Role |
|---|---|---|
| GET / POST | `/shareholders` | Admin / Shareholder GET; Admin POST |
| GET / POST | `/shareholders/rules` | Same |
| GET / POST | `/shareholders/distributions` | Same |
| PATCH | `/shareholders/distributions/:id/status` | Admin |
| GET / PUT | `/shareholders/historical-balances/:periodId` | GET via `/historical-balances`, Admin PUT |
| GET | `/reports/monthly?year=2026&month=11` | Admin / Shareholder |
| GET | `/reports/account-balances` | Admin / Shareholder |
| GET | `/reports/income-expense-trend?from=2026-11-01&to=2026-11-30` | Admin / Shareholder |

Reports are JSON suitable for Angular charts and exports. .xlsx exports are a **frontend job or future reporting module**, not implemented in this backend yet.

## Documents

- `POST /documents/financial` — Admin, `multipart/form-data`: `file`, `document_type` and exactly one of `income_id`, `expense_id`, `payroll_item_id`.
- `GET /documents/financial/:id` — Admin private download.
- `POST /documents/employee/:employeeId` — Admin or employee self, `multipart/form-data` with `file` and `document_type` (`id`, `contract`, `other`).
- `GET /documents/employee/:id/download` — Admin or owner private download.

Maximum 5 MB by default. Allowed PDFs, JPEG and PNG. Production requires malware scanning and managed/private object storage.

## Responses

Success: `{ "data": ... }` or `{ "message": ... }`. Invalid input: HTTP 422 with `{ "error":"VALIDATION_ERROR","details":[{"field":"...","message":"..."}] }`. 401 unauthorized; 403 forbidden; 404 not found; 409 conflicting record/state; 500 generic server error.

Employee bank account editing: `PATCH /employees/me/bank-details/:id` accepts the same full body as `PUT /employees/me/bank-details`. Employee file list: `GET /employees/me/documents`. Only the authenticated employee's account/document records are accessible.
