Skip to content

Supply Chain

@porulle/plugin-procurement adds inbound logistics: suppliers, purchase orders (POs), and goods received notes (GRNs). It extends the core inventory module by tracking stock from supplier to warehouse.

Terminal window
bun add @porulle/plugin-procurement
commerce.config.ts
import { procurementPlugin } from "@porulle/plugin-procurement";
export default defineConfig({
plugins: [procurementPlugin()],
});

Update drizzle.config.ts:

drizzle.config.ts
import { getSchemaFiles } from "@porulle/core";
import { createRequire } from "node:module";
const resolve = createRequire(import.meta.url).resolve;
schema: [
...getSchemaFiles(),
resolve("@porulle/plugin-procurement/schema"),
],

Push the new tables and restart:

Terminal window
bunx drizzle-kit push --config drizzle.config.ts
bun run src/server.ts

The plugin registers three scopes. Grant them to a role or an API-key scope.

| Scope | Grants | |-------|--------| | procurement:read | View POs, GRNs, suppliers | | procurement:create | Create POs and GRNs | | procurement:admin | Approve POs, manage suppliers, accept GRNs |

Supplier — a vendor you purchase inventory from. Supplier items link a supplier to the entities they can fulfil.

Purchase order — an order placed with a supplier. Status moves through draft → pending_approval → approved → sent → partially_received → received, or cancelled.

Goods received note — a receiving event against a PO, recorded as its own object rather than a field on the PO. This is what lets one PO be received in several deliveries, each with its own accepted and rejected counts. Status moves through draft → inspecting → accepted, accepted_with_discrepancy, or rejected.

Warehouse — a storage location from the core inventory module. A PO and a GRN each name the warehouse they apply to.

| Operation | Endpoint | Permission | |-----------|----------|------------| | Register supplier | POST /api/procurement/suppliers | procurement:admin | | List suppliers | GET /api/procurement/suppliers | procurement:read | | Link item to supplier | POST /api/procurement/suppliers/{id}/items | procurement:admin | | Create PO | POST /api/procurement/purchase-orders | procurement:create | | List POs | GET /api/procurement/purchase-orders | procurement:read | | Submit PO for approval | POST /api/procurement/purchase-orders/{id}/submit | procurement:create | | Approve PO | POST /api/procurement/purchase-orders/{id}/approve | procurement:admin | | Cancel PO | POST /api/procurement/purchase-orders/{id}/cancel | procurement:admin | | Create GRN | POST /api/procurement/grn | procurement:create | | Accept GRN | POST /api/procurement/grn/{id}/accept | procurement:admin |

Terminal window
curl -X POST http://localhost:4000/api/procurement/purchase-orders \
-H "content-type: application/json" \
-H "x-api-key: $PORULLE_API_KEY" \
-d '{
"supplierId": "supplier-uuid",
"warehouseId": "warehouse-uuid",
"expectedDelivery": "2026-06-01",
"items": [
{
"entityId": "product-uuid",
"itemName": "Classic Logo Tee",
"quantityOrdered": 100,
"unitCost": 1500
}
]
}'

unitCost is in minor units, like every other amount in the API. The PO is created as draft; submit it, then approve it.

Receiving is a GRN, not a field update on the PO. Each line names the PO line it fulfils and splits the delivery into accepted and rejected quantities:

Terminal window
curl -X POST http://localhost:4000/api/procurement/grn \
-H "content-type: application/json" \
-H "x-api-key: $PORULLE_API_KEY" \
-d '{
"poId": "po-uuid",
"supplierId": "supplier-uuid",
"warehouseId": "warehouse-uuid",
"notes": "Partial shipment — remaining 20 units en route",
"items": [
{
"poItemId": "po-item-uuid",
"entityId": "product-uuid",
"quantityOrdered": 100,
"quantityReceived": 80,
"quantityAccepted": 78,
"quantityRejected": 2,
"rejectionReason": "Print defect",
"unitCost": 1500
}
]
}'

Accepting the GRN is the step that moves stock:

Terminal window
curl -X POST http://localhost:4000/api/procurement/grn/$GRN_ID/accept \
-H "x-api-key: $PORULLE_API_KEY"

Inventory levels at the named warehouse rise by the accepted quantity. The PO becomes partially_received while any ordered quantity is outstanding, and received once every line is satisfied.

Splitting receipt from acceptance is deliberate: goods that arrived and goods you agreed to pay for are different quantities, and a defect found at inspection should not have already moved stock.