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.
Install
Section titled “Install”bun add @porulle/plugin-procurementRegister
Section titled “Register”import { procurementPlugin } from "@porulle/plugin-procurement";
export default defineConfig({ plugins: [procurementPlugin()],});Update 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:
bunx drizzle-kit push --config drizzle.config.tsbun run src/server.tsPermissions
Section titled “Permissions”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 |
Core objects
Section titled “Core objects”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.
Quick reference
Section titled “Quick reference”| 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 |
Create a purchase order
Section titled “Create a purchase order”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.
Receive goods
Section titled “Receive goods”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:
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:
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.
Related
Section titled “Related”- Plugin API Reference — every procurement endpoint and data type
- Build a Loyalty Plugin — how plugins extend the engine
- Custom Tables guide — add your own tables alongside plugin tables