📦 Inventory & Pricing API

Manage inventory, purchases, sales, and product pricing

Base URL

http://localhost:5000

Inventory Management

GET /inventory Admin / Employee

List all inventory items with filters and pagination.

Query Parameters

  • product_id (optional) - Filter by product
  • status (optional) - Filter by status: active, inactive, archived
  • low_stock (optional) - Set to "true" to show only items where quantity <= reorder_point
  • q (optional) - Search by product name or SKU
  • page, per_page (optional; default 1 / 20)

Example:

GET /inventory?low_stock=true&page=1&per_page=20

200 OK

{
  "inventory": [
    {
      "id": 1,
      "product_id": 1,
      "product": {"id": 1, "name": "Potatoes", "sku": "POT-001"},
      "quantity": "50.000",
      "unit_id": 3,
      "unit": {"id": 3, "name": "Kilogram", "symbol": "kg"},
      "reorder_point": "20.000",
      "status": "active",
      "created_at": "2025-01-15T10:00:00",
      "updated_at": "2025-01-15T10:00:00"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 1,
    "pages": 1,
    "has_next": false,
    "has_prev": false
  }
}
GET /inventory/<product_id> Admin / Employee

Get inventory details for a specific product.

200 OK

404 Not Found - Product or inventory not found

POST /inventory/<product_id>/initialize Admin Only

Initialize inventory tracking for a product. Can only be done if inventory doesn't already exist.

Request Body (JSON)

  • unit_id (required) - Unit ID for measuring quantity
  • quantity (optional) - Initial quantity (default: 0)
  • reorder_point (optional) - Minimum stock level before reordering

Example:

POST /inventory/1/initialize
{
  "unit_id": 3,
  "quantity": 0,
  "reorder_point": 20
}

201 Created

400 - Missing required fields or unit not found

404 - Product not found

409 - Inventory already initialized

PATCH /inventory/<product_id> Admin Only

Update inventory details (quantity, reorder_point, status). Note: Quantity is usually updated automatically by purchase/sale operations.

Request Body (JSON)

  • quantity (optional) - Update quantity manually
  • reorder_point (optional) - Update reorder point
  • status (optional) - Update status: active, inactive, archived

Example:

PATCH /inventory/1
{
  "quantity": 60,
  "reorder_point": 25
}

200 OK

404 - Product or inventory not found

Purchase Management

📝 Note: Every purchase creates an immutable record. This maintains cost history for FIFO/average cost calculations. Each purchase automatically increases inventory quantity.
POST /inventory/purchases Admin Only

Record a stock purchase. Creates a PurchaseItem record and automatically increases inventory quantity.

Request Body (JSON)

  • product_id (required) - Product ID
  • supplier_id (required) - Supplier ID who provided the stock
  • quantity (required) - Quantity purchased
  • unit_cost (required) - Cost per unit at time of purchase

Example:

POST /inventory/purchases
{
  "product_id": 1,
  "supplier_id": 5,
  "quantity": 40,
  "unit_cost": 35
}

201 Created

{
  "purchase_item": {
    "id": 1,
    "product_id": 1,
    "supplier_id": 5,
    "supplier": {
      "id": 5,
      "name": "Fresh Farm Supplies",
      "business_name": "Fresh Farm Co.",
      "email": "contact@freshfarm.com",
      "phone_number": "+254700000000"
    },
    "quantity": "40.000",
    "unit_cost": "35.000",
    "created_at": "2025-01-15T10:30:00"
  },
  "inventory": {
    "id": 1,
    "product_id": 1,
    "quantity": "90.000",
    ...
  }
}

400 - Missing required fields, invalid values, or inventory not initialized

404 - Product or supplier not found

GET /inventory/purchases Admin / Employee

List all purchase history with filters.

Query Parameters

  • product_id (optional) - Filter by product
  • supplier_id (optional) - Filter by supplier
  • date_from (optional) - Filter purchases from date
  • date_to (optional) - Filter purchases to date
  • page, per_page (optional; default 1 / 20)

Example:

GET /inventory/purchases?supplier_id=5&date_from=2025-01-01&page=1

200 OK

Returns paginated list of purchases with supplier details included.

GET /inventory/purchases/<product_id> Admin / Employee

Get purchase history for a specific product with totals (total quantity, total cost, average cost).

Query Parameters

  • page, per_page (optional; default 1 / 20)

200 OK

{
  "product": {"id": 1, "name": "Potatoes", ...},
  "purchases": [...],
  "totals": {
    "total_quantity": "60.000",
    "total_cost": "1950.000",
    "average_cost": "32.500"
  },
  "pagination": {...}
}

404 - Product not found

Sale Management

📝 Note: Every sale creates an immutable record with a price snapshot. This ensures historical accuracy even if prices change later. Each sale automatically decreases inventory quantity.
POST /inventory/sales Admin Only

Record a sale. Creates a SaleItem record with price snapshot and automatically decreases inventory quantity. Validates sufficient stock availability.

Request Body (JSON)

  • product_id (required) - Product ID
  • quantity (required) - Quantity sold
  • unit_price (required) - Price per unit at time of sale (snapshot)

Example:

POST /inventory/sales
{
  "product_id": 1,
  "quantity": 10,
  "unit_price": 60
}

201 Created

{
  "sale_item": {
    "id": 1,
    "product_id": 1,
    "product": {"id": 1, "name": "Potatoes", ...},
    "quantity": "10.000",
    "unit_price": "60.000",
    "created_at": "2025-01-15T11:00:00"
  },
  "inventory": {
    "id": 1,
    "quantity": "80.000",
    ...
  }
}

400 - Missing required fields, invalid values, insufficient stock, or inventory not initialized

404 - Product not found

GET /inventory/sales Admin / Employee

List all sale history with filters.

Query Parameters

  • product_id (optional) - Filter by product
  • date_from (optional) - Filter sales from date
  • date_to (optional) - Filter sales to date
  • page, per_page (optional; default 1 / 20)

200 OK

GET /inventory/sales/<product_id> Admin / Employee

Get sale history for a specific product with totals (total quantity, total revenue, average price).

200 OK

{
  "product": {"id": 1, "name": "Potatoes", ...},
  "sales": [...],
  "totals": {
    "total_quantity": "15.000",
    "total_revenue": "900.000",
    "average_price": "60.000"
  },
  "pagination": {...}
}

404 - Product not found

Price Management

📝 Note: Prices are time-based rules. When you change a price, create a new price rule rather than editing the old one. Old prices remain in history for reference. The active price is determined by effective_from and effective_to dates.
POST /products/<product_id>/prices Admin Only

Set a new price for a product. Creates a new price rule and optionally closes the previous active price by setting its effective_to date.

Request Body (JSON)

  • amount (required) - Price amount
  • price_type (optional) - Type: retail, promo, wholesale (default: retail)
  • effective_from (optional) - When this price becomes active (ISO format, default: now)
  • close_old_price (optional) - Set previous active price's effective_to (default: true)

Example:

POST /products/1/prices
{
  "amount": 60,
  "price_type": "retail",
  "close_old_price": true
}

201 Created

{
  "price": {
    "id": 2,
    "product_id": 1,
    "amount": "60.000",
    "price_type": "retail",
    "effective_from": "2025-01-15T12:00:00",
    "effective_to": null,
    "created_at": "2025-01-15T12:00:00",
    "updated_at": "2025-01-15T12:00:00"
  }
}

400 - Missing amount, invalid price_type, or invalid date format

404 - Product not found

GET /products/<product_id>/prices Public

List price history for a product. By default shows only active prices.

Query Parameters

  • include_inactive (optional) - Set to "true" to include inactive/expired prices (default: false)
  • page, per_page (optional; default 1 / 20)

Example:

GET /products/1/prices?include_inactive=true

200 OK

404 - Product not found

GET /products/<product_id>/prices/active Public

Get the currently active price for a product. This is the price that should be displayed to customers.

200 OK

{
  "price": {
    "id": 2,
    "product_id": 1,
    "amount": "60.000",
    "price_type": "retail",
    "effective_from": "2025-01-15T12:00:00",
    "effective_to": null,
    ...
  }
}

404 - Product not found or no active price

💡 Usage: When displaying products to customers or adding to cart, use this endpoint to get the current selling price. When recording a sale, copy this price into the unit_price field to create an immutable snapshot.

Typical Workflow

1. Add Product

Create product via POST /products (no inventory or price yet)

2. Initialize Inventory

POST /inventory/{product_id}/initialize with unit_id and optional initial quantity

3. Record First Purchase

POST /inventory/purchases with product_id, supplier_id, quantity, unit_cost

This automatically increases inventory quantity

4. Set Selling Price

POST /products/{product_id}/prices with amount and price_type

5. Customer Views Product

GET /products/{product_id} for product details

GET /products/{product_id}/prices/active for current price

GET /inventory/{product_id} for stock availability

6. Record Sale at Checkout

POST /inventory/sales with product_id, quantity, and unit_price (snapshot from step 5)

This automatically decreases inventory quantity

7. Future Operations

  • More stock arrives: Record another purchase (step 3) - price unchanged, inventory increases
  • Price change: Create new price rule (step 4) - old price preserved in history
  • More sales: Record sales (step 6) - uses current active price