# Internal Purchase Order Approval API

Human-readable documentation: `/docs`

This markdown is the agent-readable copy of the same contract.

Internal system for submitting, approving, and processing purchase requests.
On approval: generates PDF PO and creates Zammad ticket.
Designed for multi-level approval rules (thresholds, roles) that can be extended later.
Hosted on Coolify (Docker). Telegram notifications for approvers.
Human-readable docs are at /docs. Agents can read the markdown copy at /docs/api.md.

Servers:

- https://procurement.ictnetworks.co.za/api/v1

Authenticate with `POST /api/v1/auth/login`. Send the returned token as `Authorization: Bearer <token>`.

## POST /api/v1/auth/login

Login and receive JWT

Request body:

- `email` (string, required)
- `password` (string, required)

Responses:

- `200`: Successful login
- `401`: Invalid credentials

## GET /api/v1/auth/me

Get current user

Responses:

- `200`: Current user
- `401`: Not authenticated

## PATCH /api/v1/auth/me

Update own name, surname, or password

Request body:

- `name` (string, optional)
- `surname` (string, optional)
- `current_password` (string, optional)
- `new_password` (string, optional)

Responses:

- `200`: Updated user
- `400`: Invalid password or profile
- `401`: Not authenticated

## GET /api/v1/users

List users (admin only)

Responses:

- `200`: List of users

## POST /api/v1/users

Create user (admin only)

Request body:

- `email` (string, required)
- `name` (string, required)
- `surname` (string, optional)
- `password` (string, required)
- `role` (string, optional) one of requester, approver, admin
- `department` (string, optional)

Responses:

- `201`: User created
- `403`: Forbidden

## POST /api/v1/users/{user_id}/disable

Disable a user (admin)

Parameters:

- `user_id` in path (required)

Responses:

- `200`: User disabled
- `400`: Cannot disable this user
- `404`: User not found

## POST /api/v1/users/{user_id}/enable

Enable a user (admin)

Parameters:

- `user_id` in path (required)

Responses:

- `200`: User enabled
- `404`: User not found

## PATCH /api/v1/users/{user_id}

Change another user's role (admin)

Parameters:

- `user_id` in path (required)

Request body:

- `role` (string, required) one of requester, approver, admin

Responses:

- `200`: Updated user
- `400`: The change would remove the last admin, or the admin tried to change their own role
- `404`: User not found

## GET /api/v1/requests

List requests (own requests for requesters; pending + own for approvers)

Parameters:

- `status` in query (optional)
- `mine` in query (optional)

Responses:

- `200`: List of purchase requests

## POST /api/v1/requests

Submit a new purchase request

Request body:

- `department` (string, required)
- `vendor` (string, required)
- `reason` (string, required)
- `urgency` (string, optional) one of low, medium, high
- `line_items` (array, required)
  - `description` (string, required)
  - `quantity` (number, required)
  - `unit_price` (number, optional)
  - `unit_price_incl` (number, optional)
  - `price_basis` (string, optional) one of excl, incl
  - `product_url` (string, nullable, optional)
- `delivery_office_id` (integer, nullable, optional)
- `custom_address` (string, nullable, optional)

Responses:

- `201`: Request created (status=pending). Telegram notification sent.
- `400`: Validation error

## GET /api/v1/requests/{request_id}

Get request details

Parameters:

- `request_id` in path (required)

Responses:

- `200`: Request details
- `403`: Not allowed to view
- `404`: Not found

## GET /api/v1/requests/{request_id}/pdf

Download generated PO PDF (after approval or on demand)

Parameters:

- `request_id` in path (required)

Responses:

- `200`: PDF file
- `404`: Not found or not yet approved

## POST /api/v1/requests/{request_id}/quote

Upload a vendor quote (PDF, PNG, or JPG, 10 MB maximum)

Parameters:

- `request_id` in path (required)

Responses:

- `201`: Quote stored. Extraction runs when the selected provider has a token.
- `400`: Unsupported file or file larger than 10 MB
- `401`: Not authenticated
- `403`: Not allowed to view
- `404`: Not found

## GET /api/v1/requests/{request_id}/quote

Download the original quote file

Parameters:

- `request_id` in path (required)

Responses:

- `200`: Original quote file
- `401`: Not authenticated
- `403`: Not allowed to view
- `404`: No quote stored, or request not found

## POST /api/v1/requests/{request_id}/approve

Approve or reject a pending request

Parameters:

- `request_id` in path (required)

Request body:

- `action` (string, required) one of approve, reject
- `comment` (string, nullable, optional)

Responses:

- `200`: Action applied. On approve: PDF generated + Zammad ticket created.
- `400`: Not pending or invalid action
- `403`: Not an approver

## GET /api/v1/admin/rules

List approval rules

Responses:

- `200`: Approval rules

## POST /api/v1/admin/rules

Create or update approval rule (admin)

Request body:

- `id` (integer, optional)
- `min_amount` (number, optional)
- `max_amount` (number, nullable, optional)
- `required_roles` (array, optional)
  - string
- `auto_approve` (boolean, optional)
- `description` (string, optional)
- `required_approvals` (integer, optional)

Responses:

- `201`: Rule saved

## GET /api/v1/admin/smtp

Read SMTP settings (password is not returned)

Responses:

- `200`: SMTP settings

## PUT /api/v1/admin/smtp

Save SMTP settings

Request body:

- `enabled` (boolean, optional)
- `host` (string, optional)
- `port` (integer, optional)
- `username` (string, optional)
- `password` (string, optional)
- `use_tls` (boolean, optional)
- `use_ssl` (boolean, optional)
- `from_address` (string, optional)
- `from_name` (string, optional)

Responses:

- `200`: SMTP settings saved

## POST /api/v1/admin/smtp/test

Send a test email with the saved SMTP settings

Request body:

- `to` (string, required)

Responses:

- `200`: Test email sent
- `400`: Email is not configured or could not be sent

## GET /api/v1/admin/settings

Read approval settings, VAT, company details, delivery offices, and the Zammad email address

Responses:

- `200`: System settings

## PUT /api/v1/admin/settings

Save approval settings, VAT, company details, delivery offices, and the Zammad email address

Request body:

- `approval_limit` (number, nullable, optional)
- `approver_count` (integer, optional)
- `rule_id` (integer, nullable, optional)
- `description` (string, optional)
- `zammad_email` (string, optional)
- `legal_name` (string, optional)
- `registration_number` (string, optional)
- `vat_number` (string, optional)
- `phone` (string, optional)
- `vat_percent` (number, optional)
- `offices` (array, optional)
  - `id` (integer, required)
  - `name` (string, required)
  - `address` (string, required)

Responses:

- `200`: System settings saved

## GET /api/v1/admin/audit

Recent audit events

Responses:

- `200`: Audit events
