Purchase Records API

Reference for the SupplierPurchaseRecord endpoints — track procurement transactions with status lifecycle, document tracking, and automatic CO₂ emission calculation.

Endpoints

MethodPathDescription
GET/api/v1/purchasesList purchase records, filterable by status
GET/api/v1/purchases/{id}/status-changesList status change audit log
POST/api/v1/purchasesCreate a purchase record
PATCH/api/v1/purchases/{id}Update a purchase record
POST/api/v1/purchases/bulkBulk upsert purchase records

GET /api/v1/purchases

Query Parameters

ParameterTypeRequiredDescription
purchase_statusstringFilter by status: ORDERED, IN_TRANSIT, DELIVERED, CANCELLED. Omit to return all.
Shell
curl http://localhost:8080/api/v1/purchases
curl "http://localhost:8080/api/v1/purchases?purchase_status=IN_TRANSIT"
JavaScript
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.

Shell
curl http://localhost:8080/api/v1/purchases/101/status-changes

Response 200 OK

JSON
[
{
"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

FieldTypeRequiredDescription
supplier_idintegerFK to Supplier.id
supplier_product_idintegerFK to SupplierProduct.id
purchase_datestring (date-time)ISO 8601 datetime e.g. 2024-03-15T10:30:00
product_descriptionstringFree-text description
quantity_kgnumberQuantity in kg
consumed_kgnumberQuantity consumed
current_stock_kgnumberCurrent stock
purchase_value_eurnumberPurchase value in EUR
transport_methodstringROAD, SEA, AIR, RAIL
distance_kmnumberTransport distance in km
distance_sourcestringHow distance was calculated
purchase_statusstringInitial status. Default: ORDERED
purchase_order_numberstringPO reference, max 100 chars
invoice_numberstringInvoice reference, max 100 chars
ddt_numberstringDDT delivery note number, max 100 chars
transaction_certificate_numberstringTC number (GOTS/GRS), max 100 chars
batch_numberstringBatch/lot ID, unique per supplier_id
delivery_datestring (date-time)Actual delivery datetime
expected_delivery_datestring (date-time)Expected delivery datetime
delivery_notesstringFree-text delivery notes, max 2000 chars
notesstringGeneral free-text notes
metadataobjectArbitrary JSON key-value pairs
Note

transport_emissions_tco2 is a calculated field — never include it in the request body. It is computed automatically and returned in the response.

Warning

delivery_date must not be before purchase_date. If this constraint is violated the API returns 400 Bad Request.

Shell
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"
}'
JavaScript
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

JSON
{
"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:

Shell
ORDERED → IN_TRANSIT → DELIVERED
↘ ↗
CANCELLED ←──
StatusMeaning
ORDEREDPurchase placed, awaiting shipment. Default.
IN_TRANSITShipment dispatched from supplier
DELIVEREDGoods received at warehouse
CANCELLEDOrder 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}:

Shell
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:

FieldJSON KeyMax LengthExampleNotes
Purchase Orderpurchase_order_number100PO-2025-001Unique per your ERP
Invoiceinvoice_number100INV-2025-4521Supplier invoice number
DDTddt_number100DDT-IT-00123Italian delivery note
Transaction Certificatetransaction_certificate_number100TC-GOTS-2025-001GOTS/GRS certification transaction
Batch / Lotbatch_number100BATCH-2025-09-AUnique per supplier_id (enforced by DB)

Emission Factor Reference

Transport ModeEmission FactorUnit
ROAD0.000096kg CO₂ / (tonne · km)
SEA0.000011kg CO₂ / (tonne · km)
AIR0.000602kg CO₂ / (tonne · km)
RAIL0.000028kg 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).

JSON
{
"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
}
]
}