Skip to main content

Purchasing Module

The purchasing domain manages the full procurement lifecycle across three backend modules:

ModulePathPurpose
Suppliersapps/backend/src/suppliers/Vendor/partner registry
Purchase Ordersapps/backend/src/purchase-orders/Procurement documents before goods receipt
Purchasesapps/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 statuspendingpartialreceived
  • 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:

  1. OnCreateGoodsReceivedNoteEvent is emitted
  2. PurchaseOrdersListener handles the event, updates the PO's receiptSummary
  3. If all items are fully received, goodsReceivedNoteStatus transitions to received

3. Cost Calculation

On document creation/update:

  1. Items are extracted from purchaseDetail JSON
  2. Grouped by location and product
  3. Inventory records are ensured to exist
  4. Current WAC costs are fetched from inventory
  5. Cost detail is calculated and stored as costDetail JSON

API Endpoints

Suppliers — /suppliers

MethodPathDescriptionAuth
POST/suppliersCreate supplierCreate permission
GET/suppliersList suppliers (paginated)Read permission
GET/suppliers/:idGet supplier by IDRead permission
PATCH/suppliers/:idUpdate supplierUpdate permission
DELETE/suppliers/:idDelete supplierDelete 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).

MethodPathDescription
POST/purchase-ordersCreate PO
GET/purchase-ordersList POs (paginated, filtered)
GET/purchase-orders/searchSearch POs
GET/purchase-orders/:idGet PO by ID
GET/purchase-orders/:id/pdfDownload PO as PDF
GET/purchase-orders/:id/printHTML print preview
PATCH/purchase-orders/:idUpdate PO
DELETE/purchase-orders/:idDelete PO (requires businessId query)

Filters: status, goodsReceivedNoteStatus, createdAt range, orderDate range.

Purchases — /purchases

MethodPathDescription
POST/purchasesCreate purchase
GET/purchasesList purchases (paginated, filtered)
GET/purchases/searchSearch purchases
GET/purchases/:idGet purchase by ID
GET/purchases/:id/pdfDownload purchase as PDF
GET/purchases/:id/pdf/contentGet PDF as base64 + metadata
GET/purchases/:id/printHTML print preview
PATCH/purchases/:idUpdate purchase
DELETE/purchases/:idDelete purchase

Filters: status, documentNumber, createdAt range, purchaseDate range.

Database Tables

TableKey columns
supplierid, businessId, name, taxId, taxName, taxAddress, contact (JSON), supplierCode
purchaseOrderid, businessId, supplierId, purchaseDetail (JSON), costDetail (JSON), receiptSummary (JSON), status, goodsReceivedNoteStatus, documentNumber
purchaseid, 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

  1. Variant validation — PO creation validates that referenced product variants are active and belong to the correct product (done in repository layer).
  2. Receipt tracking — POs maintain a receiptSummary JSON that is updated via events when GRNs are created, avoiding tight coupling.
  3. Cost snapshotcostDetail captures WAC costs at document time, providing an audit trail independent of inventory cost changes.
  4. PDF generation — Uses Puppeteer via a shared PdfModule. Supports configurable page size, margins, and template overrides.
  • goods-received-notes/ — GRN documents that trigger PO receipt updates
  • inventories/ — Inventory records and WAC cost data
  • pdf/ — Shared PDF generation infrastructure