Skip to main content

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=…) stays SupplierDto, 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 uses BillingCommandClient (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.

  • pharmacy — current clinical SCM implementer
  • billing — OTC SALE charges (not a procurement concern)
  • concept — MEDICAL conceptId on 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.