Procurement — How it works
Overview
Procurement is a shared contract, not a pharmacy-owned type family. Supplier, purchase-order,
goods-receipt, supplier-return and reorder-suggestion DTOs live in
exchange.dto.procurement. Shared names (MODULE_NAME, anonymous-sale invoice prefix) live in
common.procurement. Pharmacy implements clinical SCM against those types today (REST still
under /api/v1/pharmacy/* so the till UI does not move). Health ERP will implement general /
non-clinical procurement against the same contracts under /api/v1/procurement
(ApiV1Paths.PROCUREMENT).
Data model & ownership
Tables stay with the implementing module. Pharmacy's clinical SCM tables are
pharmacy.supplier, purchase_order / purchase_order_line, goods_receipt /
goods_receipt_line, and supplier_return (migration pharmacy/004). ERP will own its own
schema when that engine lands. Cross-module references (product, concept, facility) are
logical — never a foreign key into another module.
Clinical identity on a MEDICAL line is the catalog product's conceptId. Product kind
(MEDICAL / NON_MEDICAL) stays a pharmacy catalog enum; procurement lines reference it
read-only.
Key rules & invariants
- Contracts belong to procurement. Do not add supplier / PO / GRN types under
exchange.dto.pharmacy. OpenAPI@Schema(name=…)staysSupplierDto,PurchaseOrderDto, and so on so existing frontend Zod types do not rename. - Supplier is TypeScope CRUD. A purchase order names a
supplierId— never a free-text supplier name. Status and return reason are closed enums. - Goods receipt is the purchase stock-increase path in the implementing module.
- Pharmacy never imports
core. When a till sale needs a bill, it usesBillingCommandClient(ChargeSourceType.SALE); that is billing, not procurement.
API
Pharmacy clinical SCM: /api/v1/pharmacy/suppliers, /purchase-orders, /goods-receipts,
/supplier-returns, /reorder-suggestions. Future ERP: /api/v1/procurement/* on the same
DTO shapes. See the API Reference.
Configuration & feature flags
None at the contract layer. Implementing modules resolve Feign URLs as usual.
Related features
pharmacy— current clinical SCM implementerbilling— OTCSALEcharges (not a procurement concern)concept— MEDICALconceptIdon catalog products referenced by PO / GRN lines
Why it is this way
DTOs live in exchange.dto.procurement once, implemented many times. Pharmacy serves clinical
SCM under /api/v1/pharmacy/* today; ERP binds /api/v1/procurement/* on the same shapes. OpenAPI
@Schema(name=…) stays stable so frontend Zod types do not rename between modules.
Supplier is TypeScope CRUD. Purchase orders reference supplierId; free-text vendor names forfeit
CHECK constraints and break three-way match.
Goods receipt is the stock-increase path in whichever module implements procurement — not a parallel adjustment API. Returns and reorder suggestions share the same contract family.
Traps
⚠ Never add supplier/PO/GRN types under exchange.dto.pharmacy — pharmacy imports procurement
contracts; duplicating types forks the API surface.
⚠ Three-way match refuses over-receipt — GRN quantity above PO open quantity is 409, not a silent cap; finance depends on match status.
⚠ MEDICAL/formulary lines belong in pharmacy, not ERP general procurement — ERP must refuse clinical product categories with a pointer to M19 (M21-006 guard).
⚠ Bean and entity name collisions when ERP and pharmacy load together — disambiguate Spring bean names and JPA entity names for goods receipt controllers/entities.