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/purchase-recordsList purchase records, filterable by status
POST/api/purchase-recordsCreate a purchase record
PATCH/api/purchase-records/{id}Update a purchase record
POST/api/purchase-records/bulkBulk upsert purchase records

Required Headers

HeaderRequiredDescription
X-User-Id✅ on POST / PATCHID of the user performing the action (audit log)
X-API-Key✅ all requestsService authentication key

GET /api/purchase-records

Query Parameters

ParameterTypeRequiredDescription
purchase_statusstringFilter by status: ORDERED, IN_TRANSIT, DELIVERED, CANCELLED. Omit to return all.
Shell
# All purchase records
curl http://localhost:8080/api/purchase-records \
-H "X-API-Key: your-api-key"
# Only records currently in transit
curl "http://localhost:8080/api/purchase-records?purchase_status=IN_TRANSIT" \
-H "X-API-Key: your-api-key"
JavaScript
const records = await fetch(
'/api/purchase-records?purchase_status=IN_TRANSIT',
{ headers: { 'X-API-Key': 'your-api-key' } }
).then(r => r.json());

POST /api/purchase-records

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 (replaces purchase_year)
quantity_kgnumberQuantity in kg
unit_pricenumberPrice per unit
currencystringISO 4217 currency code
transport_modestringROAD, SEA, AIR, RAIL
distance_kmnumberTransport distance in km
weight_kgnumberShipment weight in kg
purchase_statusstringInitial status. Default: ORDERED
purchase_order_numberstringPO reference, max 100 chars
invoice_numberstringInvoice reference, max 100 chars
ddt_numberstringDDT (Documento di Trasporto) delivery note number, max 100 chars
transaction_certificate_numberstringTC number (GOTS/GRS), max 100 chars
batch_numberstringBatch/lot ID for traceability, max 100 chars. 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
Note

co2_emissions_kg is a calculated field — never include it in the request body. It is computed from transport_mode, distance_km, and weight_kg 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/purchase-records \
-H "Content-Type: application/json" \
-H "X-User-Id: 1" \
-H "X-API-Key: your-api-key" \
-d '{
"supplier_id": 1,
"supplier_product_id": 5,
"purchase_date": "2025-09-15T09:00:00",
"quantity_kg": 500.0,
"transport_mode": "SEA",
"distance_km": 8500,
"weight_kg": 600,
"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/purchase-records', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-User-Id': '1',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify({
supplier_id: 1,
supplier_product_id: 5,
purchase_date: '2025-09-15T09:00:00',
quantity_kg: 500.0,
transport_mode: 'SEA',
distance_km: 8500,
weight_kg: 600,
purchase_order_number: 'PO-2025-001',
batch_number: 'BATCH-2025-09-A'
})
});
const record = await resp.json();
console.log(`Status: ${record.purchase_status}, CO₂: ${record.co2_emissions_kg} kg`);
Python
import requests
resp = requests.post(
'http://localhost:8080/api/purchase-records',
headers={'X-User-Id': '1', 'X-API-Key': 'your-api-key'},
json={
'supplier_id': 1,
'purchase_date': '2025-09-15T09:00:00',
'quantity_kg': 500.0,
'transport_mode': 'SEA',
'distance_km': 8500,
'weight_kg': 600,
'purchase_order_number': 'PO-2025-001',
}
)
record = resp.json()
print(f"CO₂: {record['co2_emissions_kg']} kg, Status: {record['purchase_status']}")

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_mode": "SEA",
"distance_km": 8500.0,
"weight_kg": 600.0,
"co2_emissions_kg": 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,
"delivery_notes": 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 X-User-Id header. This provides a full audit trail.

To update the status, use PATCH /api/purchase-records/{id}:

Shell
curl -X PATCH http://localhost:8080/api/purchase-records/101 \
-H "Content-Type: application/json" \
-H "X-User-Id: 1" \
-H "X-API-Key: your-api-key" \
-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 (Documento di Trasporto)
Transaction Certificatetransaction_certificate_number100TC-GOTS-2025-001GOTS/GRS certification transaction
Batch / Lotbatch_number100BATCH-2025-09-AUnique per supplier_id (enforced by DB constraint)

All five fields share the same validation pattern: must start with an alphanumeric character and can contain letters, digits, spaces, ., /, _, -.


Migration: purchase_yearpurchase_date

JSON
{
"purchase_date": "2025-09-15T09:00:00"
}
JSON
{
"purchase_year": 2025
}

If you need to preserve only the year from your existing data, use midnight on January 1st:

Shell
2025 → "2025-01-01T00:00:00"

The DB migration V2__replace_purchase_year_with_purchase_date.sql handles the column transformation automatically on deploy.


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/purchase-records/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_mode": "AIR",
"distance_km": 1200,
"weight_kg": 50
}
]
}