Purchasing Module
The purchasing domain manages the full procurement lifecycle across three backend modules:
| Module | Path | Purpose |
|---|---|---|
| Suppliers | apps/backend/src/suppliers/ | Vendor/partner registry |
| Purchase Orders | apps/backend/src/purchase-orders/ | Procurement documents before goods receipt |
| Purchases | apps/backend/src/purchases/ | Goods received documents |
Architecture
All three modules follow hexagonal architecture:
<module>/
├── <module>.module.ts # NestJS module definition
├── application/
│ ├── <module>.service.ts # Business logic (use cases)
│ ├── <module>.listener.ts # Event listeners (cross-module integration)
│ └── events/ # Domain events
├── domain/
│ └── <module>-repository.domain.ts # Repository interface (port)
├── infrastructure/
│ └── <module>.repository.ts # Kysely DB implementation (adapter)
└── interfaces/
├── <module>.controller.ts # HTTP routes (adapter)
├── dtos/ # Request validation DTOs
└── query/ # Query parameter classes
Dependency flow: interfaces → application → domain ← infrastructure
Domain Concepts
Supplier
A vendor or partner from which goods are purchased. Contains tax information (taxId, taxName, taxAddress), contact details, and geographic location. All operations are scoped by businessId.
Purchase Order (PO)
A procurement document created before goods are received. Tracks:
- Order items with product/variant details, quantities, and pricing
- Receipt summary — ordered vs. received quantities per item (updated by GRN events)
- Goods received note status —
pending→partial→received - Document number — auto-generated unique identifier
Purchase
A goods-received document recording physical receipt of items from a supplier. Tracks:
- Purchase items with inventory detail (batch, serial, location allocation)
- Cost detail — WAC-based cost snapshot at document creation time
- Document number — auto-generated
Main Flows
1. Standard Procurement Flow
Create Supplier → Create PO → Create GRN → PO receipt summary auto-updated
2. GRN → PO Integration (Event-Driven)
When a Goods Received Note is created:
OnCreateGoodsReceivedNoteEventis emittedPurchaseOrdersListenerhandles the event, updates the PO'sreceiptSummary- If all items are fully received,
goodsReceivedNoteStatustransitions toreceived
3. Cost Calculation
On document creation/update:
- Items are extracted from
purchaseDetailJSON - Grouped by location and product
- Inventory records are ensured to exist
- Current WAC costs are fetched from inventory
- Cost detail is calculated and stored as
costDetailJSON
API Endpoints
Suppliers — /suppliers
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /suppliers | Create supplier | Create permission |
| GET | /suppliers | List suppliers (paginated) | Read permission |
| GET | /suppliers/:id | Get supplier by ID | Read permission |
| PATCH | /suppliers/:id | Update supplier | Update permission |
| DELETE | /suppliers/:id | Delete supplier | Delete permission |
All endpoints require businessId for multi-tenancy scoping.
Purchase Orders — /purchase-orders
All endpoints are protected by RolesGuard with PolicyResource.Purchase and require appropriate PolicyAction permission (Create/Read/Update/Delete).
| Method | Path | Description |
|---|---|---|
| POST | /purchase-orders | Create PO |
| GET | /purchase-orders | List POs (paginated, filtered) |
| GET | /purchase-orders/search | Search POs |
| GET | /purchase-orders/:id | Get PO by ID |
| GET | /purchase-orders/:id/pdf | Download PO as PDF |
| GET | /purchase-orders/:id/print | HTML print preview |
| PATCH | /purchase-orders/:id | Update PO |
| DELETE | /purchase-orders/:id | Delete PO (requires businessId query) |
Filters: status, goodsReceivedNoteStatus, createdAt range, orderDate range.
Purchases — /purchases
| Method | Path | Description |
|---|---|---|
| POST | /purchases | Create purchase |
| GET | /purchases | List purchases (paginated, filtered) |
| GET | /purchases/search | Search purchases |
| GET | /purchases/:id | Get purchase by ID |
| GET | /purchases/:id/pdf | Download purchase as PDF |
| GET | /purchases/:id/pdf/content | Get PDF as base64 + metadata |
| GET | /purchases/:id/print | HTML print preview |
| PATCH | /purchases/:id | Update purchase |
| DELETE | /purchases/:id | Delete purchase |
Filters: status, documentNumber, createdAt range, purchaseDate range.
Database Tables
| Table | Key columns |
|---|---|
supplier | id, businessId, name, taxId, taxName, taxAddress, contact (JSON), supplierCode |
purchaseOrder | id, businessId, supplierId, purchaseDetail (JSON), costDetail (JSON), receiptSummary (JSON), status, goodsReceivedNoteStatus, documentNumber |
purchase | id, businessId, supplierId, purchaseDetail (JSON), costDetail (JSON), paymentDetail (JSON), status, documentNumber |
Enums
- PurchaseStatus:
draft,submitted,reviewed - PurchaseOrderStatus:
draft,submitted,reviewed,approved,canceled,completed - ReceivedStatus:
pending,partial,received - PaymentStatus:
pending,partial,completed
Key Design Decisions
- Variant validation — PO creation validates that referenced product variants are active and belong to the correct product (done in repository layer).
- Receipt tracking — POs maintain a
receiptSummaryJSON that is updated via events when GRNs are created, avoiding tight coupling. - Cost snapshot —
costDetailcaptures WAC costs at document time, providing an audit trail independent of inventory cost changes. - PDF generation — Uses Puppeteer via a shared
PdfModule. Supports configurable page size, margins, and template overrides.
Related Modules
goods-received-notes/— GRN documents that trigger PO receipt updatesinventories/— Inventory records and WAC cost datapdf/— Shared PDF generation infrastructure