Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/purchases | List purchase records, filterable by status |
GET | /api/v1/purchases/{id}/status-changes | List status change audit log |
POST | /api/v1/purchases | Create a purchase record |
PATCH | /api/v1/purchases/{id} | Update a purchase record |
POST | /api/v1/purchases/bulk | Bulk upsert purchase records |
GET /api/v1/purchases
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
purchase_status | string | ❌ | Filter by status: ORDERED, IN_TRANSIT, DELIVERED, CANCELLED. Omit to return all. |
curl http://localhost:8080/api/v1/purchasescurl "http://localhost:8080/api/v1/purchases?purchase_status=IN_TRANSIT"
const records = await fetch('/api/v1/purchases?purchase_status=IN_TRANSIT').then(r => r.json());
GET /api/v1/purchases/{id}/status-changes
Returns the full audit trail of status transitions for a purchase record.
curl http://localhost:8080/api/v1/purchases/101/status-changes
Response 200 OK
[{"purchase_id": 101,"from_status": null,"to_status": "ORDERED","changed_by_user_id": 1,"changed_at": "2025-09-15T09:12:00"},{"purchase_id": 101,"from_status": "ORDERED","to_status": "IN_TRANSIT","changed_by_user_id": 1,"changed_at": "2025-09-20T14:30:00"}]
POST /api/v1/purchases
Creates a new purchase record. CO₂ emissions are calculated automatically when transport_mode, distance_km, and weight_kg are all provided. A PurchaseStatusChangeLog entry is created automatically when the status transitions.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
supplier_id | integer | ✅ | FK to Supplier.id |
supplier_product_id | integer | ❌ | FK to SupplierProduct.id |
purchase_date | string (date-time) | ✅ | ISO 8601 datetime e.g. 2024-03-15T10:30:00 |
product_description | string | ❌ | Free-text description |
quantity_kg | number | ❌ | Quantity in kg |
consumed_kg | number | ❌ | Quantity consumed |
current_stock_kg | number | ❌ | Current stock |
purchase_value_eur | number | ❌ | Purchase value in EUR |
transport_method | string | ❌ | ROAD, SEA, AIR, RAIL |
distance_km | number | ❌ | Transport distance in km |
distance_source | string | ❌ | How distance was calculated |
purchase_status | string | ❌ | Initial status. Default: ORDERED |
purchase_order_number | string | ❌ | PO reference, max 100 chars |
invoice_number | string | ❌ | Invoice reference, max 100 chars |
ddt_number | string | ❌ | DDT delivery note number, max 100 chars |
transaction_certificate_number | string | ❌ | TC number (GOTS/GRS), max 100 chars |
batch_number | string | ❌ | Batch/lot ID, unique per supplier_id |
delivery_date | string (date-time) | ❌ | Actual delivery datetime |
expected_delivery_date | string (date-time) | ❌ | Expected delivery datetime |
delivery_notes | string | ❌ | Free-text delivery notes, max 2000 chars |
notes | string | ❌ | General free-text notes |
metadata | object | ❌ | Arbitrary JSON key-value pairs |
transport_emissions_tco2 is a calculated field — never include it in the request body. It is computed automatically and returned in the response.
delivery_date must not be before purchase_date. If this constraint is violated the API returns 400 Bad Request.
curl -X POST "http://localhost:8080/api/v1/purchases?userId=1" \-H "Content-Type: application/json" \-d '{"supplier_id": 1,"supplier_product_id": 5,"purchase_date": "2025-09-15T09:00:00","quantity_kg": 500.0,"transport_method": "SEA","distance_km": 8500,"purchase_order_number": "PO-2025-001","invoice_number": "INV-2025-4521","batch_number": "BATCH-2025-09-A","purchase_status": "ORDERED"}'
const resp = await fetch('/api/v1/purchases?userId=1', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({supplier_id: 1,supplier_product_id: 5,purchase_date: '2025-09-15T09:00:00',quantity_kg: 500.0,transport_method: 'SEA',distance_km: 8500,purchase_order_number: 'PO-2025-001',batch_number: 'BATCH-2025-09-A'})});const record = await resp.json();
Response 201 Created
{"id": 101,"supplier_id": 1,"supplier_product_id": 5,"purchase_date": "2025-09-15T09:00:00","quantity_kg": 500.0,"transport_method": "SEA","distance_km": 8500.0,"transport_emissions_tco2": 0.0561,"purchase_order_number": "PO-2025-001","invoice_number": "INV-2025-4521","batch_number": "BATCH-2025-09-A","purchase_status": "ORDERED","delivery_date": null,"expected_delivery_date": null,"metadata": null,"created_at": "2025-09-15T09:12:00","updated_at": "2025-09-15T09:12:00"}
Purchase Status Lifecycle
Every purchase record has a purchase_status field that tracks its lifecycle. Valid transitions:
ORDERED → IN_TRANSIT → DELIVERED↘ ↗CANCELLED ←──
| Status | Meaning |
|---|---|
ORDERED | Purchase placed, awaiting shipment. Default. |
IN_TRANSIT | Shipment dispatched from supplier |
DELIVERED | Goods received at warehouse |
CANCELLED | Order cancelled before or during delivery |
Every status change is automatically recorded in the PurchaseStatusChangeLog table with the changed_by_user_id from the userId query parameter. This provides a full audit trail accessible via GET /api/v1/purchases/{id}/status-changes.
To update the status, use PATCH /api/v1/purchases/{id}:
curl -X PATCH "http://localhost:8080/api/v1/purchases/101?userId=1" \-H "Content-Type: application/json" \-d '{"purchase_status": "IN_TRANSIT","expected_delivery_date": "2025-10-05T08:00:00"}'
Document Tracking Fields
The following reference fields allow matching a purchase record to physical documents:
| Field | JSON Key | Max Length | Example | Notes |
|---|---|---|---|---|
| Purchase Order | purchase_order_number | 100 | PO-2025-001 | Unique per your ERP |
| Invoice | invoice_number | 100 | INV-2025-4521 | Supplier invoice number |
| DDT | ddt_number | 100 | DDT-IT-00123 | Italian delivery note |
| Transaction Certificate | transaction_certificate_number | 100 | TC-GOTS-2025-001 | GOTS/GRS certification transaction |
| Batch / Lot | batch_number | 100 | BATCH-2025-09-A | Unique per supplier_id (enforced by DB) |
Emission Factor Reference
| Transport Mode | Emission Factor | Unit |
|---|---|---|
ROAD | 0.000096 | kg CO₂ / (tonne · km) |
SEA | 0.000011 | kg CO₂ / (tonne · km) |
AIR | 0.000602 | kg CO₂ / (tonne · km) |
RAIL | 0.000028 | kg CO₂ / (tonne · km) |
Formula: CO₂ = factor × distance_km × (weight_kg / 1000)
POST /api/v1/purchases/bulk
Upserts by the combination of (supplier_id, batch_number) when batch_number is provided, otherwise by (supplier_id, supplier_product_id, purchase_date).
{"items": [{"supplier_id": 1,"supplier_product_id": 5,"purchase_date": "2025-09-15T09:00:00","quantity_kg": 500.0,"purchase_order_number": "PO-2025-001","batch_number": "BATCH-2025-09-A"},{"supplier_id": 2,"purchase_date": "2025-09-15T09:00:00","quantity_kg": 200.0,"transport_method": "AIR","distance_km": 1200}]}