Purchasing Module
The purchasing domain manages the full procurement lifecycle across four 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 |
| Purchase Document Ingestion | apps/backend/src/purchase-document-ingestion/ | FEL XML/PDF inbox → optional draft purchase |
Architecture
All four modules follow hexagonal architecture (ingestion uses discrete use-case classes instead of a single *.service.ts):
<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
Purchase document ingestion
A captured supplier invoice (FEL XML and/or PDF) that can later post as a draft purchase. See Purchase Document Ingestion.
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. Supplier invoice inbox → draft purchase
Merchants can capture certified FEL XML (optional PDF) without retyping:
Upload or email XML/PDF → extract draft → map lines → POST :id/post → purchase (draft)
Stock still moves only when that purchase is submitted. Architecture, HTTP surface, and posting constraints: Purchase Document Ingestion. Operations: Purchase Document Ingestion Troubleshooting.
4. 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.
Purchase document ingestions — /purchase-document-ingestions
Full route table, auth, and posting rules: Purchase Document Ingestion. Public email webhook is POST /webhooks/sendgrid/inbound-purchase-documents (inbox token in To, not Firebase).
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
purchase-document-ingestion/— supplier invoice capture and draft-purchase postinggoods-received-notes/— GRN documents that trigger PO receipt updatesinventories/— Inventory records and WAC cost datapdf/— Shared PDF generation infrastructure