Skip to content

Latest commit

 

History

History
315 lines (266 loc) · 14.8 KB

API.md

File metadata and controls

315 lines (266 loc) · 14.8 KB

API Contract

Owner: Member 1 (Member 4 owns the Auth and Dashboard sections). This contract is frozen at kickoff. Frontend members build against mocks shaped exactly like these examples. Changing a field name = message the whole team first, then update this file in the same PR. Live docs when the backend runs: http://localhost:8000/docs (Swagger, generated by FastAPI).

Conventions

Topic Rule
Base path /api (the frontend calls relative /api/...; Next.js rewrites proxy it to FastAPI)
Naming snake_case everywhere: JSON fields, query params. IDs are integers named <entity>_id
Auth httpOnly cookie ss_session (JWT, HS256, 8 h). Set by /auth/login, cleared by /auth/logout. SameSite=Lax, Secure in production
Quantities Decimal strings in responses ("50.000"); requests accept number or string. Max 3 decimals
Dates ISO 8601. Timestamps UTC with Z: "2026-09-26T10:15:00Z"; plain dates "2026-09-26"
Pagination Query ?page=1&page_size=20 (max 100). Response { "items": [...], "total": 57, "page": 1, "page_size": 20 }
Search search= does a case-insensitive match on SKU/name (products) or reference/partner (operations)
Content type application/json (except CSV export)

Errors

Every non-2xx response has the same shape:

{
  "code": "INSUFFICIENT_STOCK",
  "message": "Not enough stock for 1 line.",
  "details": { "lines": [ { "product_id": 12, "sku": "STL001", "available": "8.000", "requested": "10.000" } ] },
  "field_errors": []
}

Validation example (bad form input):

{
  "code": "VALIDATION_ERROR",
  "message": "Please fix the highlighted fields.",
  "details": {},
  "field_errors": [
    { "field": "email", "message": "Enter a valid email address." },
    { "field": "lines.0.quantity", "message": "Quantity must be greater than 0." }
  ]
}
HTTP code When
422 VALIDATION_ERROR Body/query failed validation (Pydantic or service rule)
422 SAME_LOCATION Transfer source = destination
401 UNAUTHORIZED Missing/expired session, or bad credentials
403 FORBIDDEN Role not allowed (e.g. staff validating an adjustment)
404 NOT_FOUND Unknown id
409 DUPLICATE Unique field taken (SKU, email, warehouse code, location name); details.field says which
409 INSUFFICIENT_STOCK Delivery/transfer exceeds available stock at validate time
409 INVALID_STATE Action not allowed in current status (edit a done op, cancel a done op, …)
400 OTP_INVALID / OTP_EXPIRED Wrong or expired reset code
429 TOO_MANY_ATTEMPTS Login locked / OTP attempts exhausted
500 INTERNAL_ERROR Unexpected; details.request_id for log lookup

Frontend rule: show field_errors inline under the matching input, and message as a toast.

Roles

auth = any logged-in user. manager = manager only. See INVENTORY_RULES § Roles.


Health

Method Path Access Returns
GET /api/health public { "status": "ok", "db": "ok" }

Auth (Member 4)

Method Path Access Body Returns
POST /api/auth/signup public { name, email, password } 201 User + sets cookie
POST /api/auth/login public { email, password } 200 User + sets cookie
POST /api/auth/logout auth — 204, clears cookie
GET /api/auth/me auth — User
PATCH /api/auth/me auth { name } User
POST /api/auth/forgot-password public { email } 200 { "message": "If the account exists, a code was sent." } (always)
POST /api/auth/verify-otp public { email, otp } 200 { "valid": true } (lets the UI move to step 3; does not consume the OTP)
POST /api/auth/reset-password public { email, otp, new_password } 200 { "message": "Password updated." }; OTP consumed
// User
{ "id": 1, "name": "Asha Manager", "email": "manager@stocksense.dev", "role": "manager", "created_at": "2026-09-26T09:00:00Z" }

Categories

Method Path Access Body / Query Returns
GET /api/categories auth — Category[] (not paginated)
POST /api/categories manager { name } 201 Category
{ "id": 1, "name": "Raw Material" }

Products

Method Path Access Body / Query Returns
GET /api/products auth search, category_id, stock_status (in_stock|low|out), is_active, page, page_size Page<Product>
POST /api/products manager ProductCreate 201 Product
GET /api/products/{product_id} auth — ProductDetail
PATCH /api/products/{product_id} manager any ProductCreate field except initial stock, plus is_active Product

No DELETE: deactivate with { "is_active": false }.

// ProductCreate
{
  "sku": "STL001", "name": "Steel Rod", "category_id": 1, "uom": "kg",
  "min_qty": "20", "max_qty": "200", "description": null,
  "initial_quantity": "0", "initial_location_id": null
}
// Product
{
  "id": 12, "sku": "STL001", "name": "Steel Rod",
  "category": { "id": 1, "name": "Raw Material" },
  "uom": "kg", "min_qty": "20.000", "max_qty": "200.000",
  "on_hand": "77.000", "stock_status": "in_stock", "suggested_order": "0.000",
  "is_active": true, "created_at": "2026-09-26T09:00:00Z"
}
// ProductDetail = Product + stock by location + last 10 ledger rows
{
  "...": "all Product fields",
  "stock_by_location": [
    { "location_id": 1, "location_name": "WH/Stock", "warehouse_id": 1, "quantity": "60.000" },
    { "location_id": 4, "location_name": "WH/Production Floor", "warehouse_id": 1, "quantity": "17.000" }
  ],
  "recent_moves": [ "LedgerEntry", "..." ]
}

stock_status: out if on_hand = 0, low if 0 < on_hand < min_qty, else in_stock.

Warehouses & Locations

Method Path Access Body / Query Returns
GET /api/warehouses auth is_active Warehouse[]
POST /api/warehouses manager { code, name, address } 201 Warehouse
PATCH /api/warehouses/{warehouse_id} manager { name, address, is_active } Warehouse
GET /api/locations auth warehouse_id, is_active Location[]
POST /api/locations manager { warehouse_id, name } 201 Location
PATCH /api/locations/{location_id} manager { name, is_active } Location (409 INVALID_STATE if deactivating a location with stock)

Creating a warehouse also creates its default location Stock.

// Warehouse
{ "id": 1, "code": "WH", "name": "Main Warehouse", "address": "Plot 4, Jalandhar", "is_active": true, "location_count": 4 }
// Location
{ "id": 2, "warehouse_id": 1, "warehouse_code": "WH", "name": "Rack A", "full_name": "WH/Rack A", "is_active": true }

Inventory

Method Path Access Query Returns
GET /api/inventory auth product_id, location_id, warehouse_id, category_id, search, hide_zero (default true), page, page_size Page<Balance>
GET /api/inventory/available auth product_id, location_id (both required) { "product_id": 12, "location_id": 1, "quantity": "60.000" }, used by forms to show "Available: 60 kg"
GET /api/inventory/integrity auth — { "ok": true, "checked": 42, "mismatches": [] }
GET /api/inventory/count-priority auth warehouse_id, limit (1–100, default 20) CountPriorityItem[]: which product-locations to physically count next, highest score first
// Balance
{ "product_id": 12, "sku": "STL001", "product_name": "Steel Rod", "uom": "kg",
  "location_id": 1, "location_name": "WH/Stock", "warehouse_id": 1, "quantity": "60.000" }
// CountPriorityItem: explainable rule score (see backend/app/services/count_priority_service.py)
{ "product_id": 12, "sku": "STL001", "product_name": "Steel Rod", "uom": "kg",
  "location_id": 4, "location_name": "WH/Production Floor", "quantity": "17.000",
  "last_counted_at": null, "days_since_count": 3, "movements_since_count": 7, "past_discrepancies": 0,
  "score": 18, "level": "low",
  "reasons": ["Never counted (first movement 3 days ago)", "7 movements since last count"] }

score = 100 × (0.40 × movements since last count ÷ 20 + 0.35 × days since last count ÷ 30 + 0.25 × past count discrepancies ÷ 3), each factor capped at 1. level: high ≥ 60, medium ≥ 30, else low. Opening stock is not a discrepancy.

Operations

Method Path Access Body / Query Returns
GET /api/operations auth type, status (comma-separated ok), warehouse_id, location_id, category_id, search, date_from, date_to, page, page_size Page<OperationSummary>
POST /api/operations auth OperationCreate 201 Operation (status draft)
GET /api/operations/{operation_id} auth — Operation
PATCH /api/operations/{operation_id} auth OperationCreate minus type (lines replaced wholesale) Operation; 409 INVALID_STATE unless draft
POST /api/operations/{operation_id}/confirm auth — Operation (ready or waiting)
POST /api/operations/{operation_id}/validate auth (manager for adjustments) — ValidateResult
POST /api/operations/{operation_id}/cancel auth — Operation (canceled)
// OperationCreate: receipt
{
  "type": "receipt",
  "destination_location_id": 1,
  "partner_name": "Tata Steel Ltd",
  "scheduled_date": "2026-09-26",
  "notes": null,
  "lines": [ { "product_id": 12, "quantity": "100" } ]
}
// delivery:   "source_location_id" + "partner_name" (customer), lines with "quantity"
// transfer:   "source_location_id" + "destination_location_id", lines with "quantity"
// adjustment: "source_location_id" (counted location), lines with "counted_quantity"
{ "type": "adjustment", "source_location_id": 4, "lines": [ { "product_id": 12, "counted_quantity": "17" } ] }
// Operation
{
  "id": 31, "reference": "WH/IN/0001", "type": "receipt", "status": "done",
  "warehouse_id": 1,
  "source_location": null,
  "destination_location": { "id": 1, "full_name": "WH/Stock" },
  "partner_name": "Tata Steel Ltd", "scheduled_date": "2026-09-26", "notes": null,
  "lines": [
    { "id": 50, "product_id": 12, "sku": "STL001", "product_name": "Steel Rod", "uom": "kg",
      "quantity": "100.000", "counted_quantity": null, "system_quantity": null,
      "available": "100.000" }
  ],
  "created_by": { "id": 1, "name": "Asha Manager" },
  "validated_by": { "id": 1, "name": "Asha Manager" },
  "created_at": "2026-09-26T09:45:00Z", "validated_at": "2026-09-26T09:46:10Z"
}
// OperationSummary = id, reference, type, status, partner_name, source_location, destination_location,
//                    scheduled_date, line_count, created_at, validated_at
// ValidateResult = Operation + effects
{
  "...": "all Operation fields",
  "already_done": false,
  "stock_effects": [
    { "product_id": 12, "sku": "STL001", "location_id": 1, "location_name": "WH/Stock",
      "quantity_delta": "100.000", "balance_after": "100.000" }
  ]
}
  • lines[].available = current balance at the source location (delivery/transfer/adjustment) or at the destination (receipt), so the form can warn early. The server re-checks on validate anyway.
  • INSUFFICIENT_STOCK lists every short line in details.lines (line_id, product_id, sku, available, requested) so the UI can highlight them all at once.
  • PATCH rules: omitted fields are kept; sending lines replaces all lines.
  • Validating an already-done operation returns 200 with "already_done": true and an empty stock_effects. No second stock change (safe for double-clicks and retries).

Move history (ledger)

Method Path Access Query Returns
GET /api/ledger auth product_id, location_id, warehouse_id, movement_type, operation_id, search, date_from, date_to, page, page_size Page<LedgerEntry> (newest first)
GET /api/ledger/export.csv auth same filters text/csv download (max 50 000 rows; text cells starting with = + - @ are prefixed with ' against spreadsheet formula injection)
// LedgerEntry
{
  "id": 204, "created_at": "2026-09-26T10:12:00Z",
  "operation_id": 33, "reference": "WH/INT/0001", "movement_type": "transfer",
  "product_id": 12, "sku": "STL001", "product_name": "Steel Rod", "uom": "kg",
  "location_id": 1, "location_name": "WH/Stock",
  "counterpart_location_id": 4, "counterpart_location_name": "WH/Production Floor",
  "quantity_delta": "-40.000", "balance_after": "60.000",
  "created_by": { "id": 2, "name": "Ravi Staff" }
}

Dashboard (Member 4)

Method Path Access Query Returns
GET /api/dashboard/summary auth warehouse_id, category_id DashboardSummary

The dashboard's operations table reuses GET /api/operations with type / status / warehouse_id / location_id / category_id filters (the PDF's "dynamic filters").

{
  "kpis": {
    "products_in_stock": 5,
    "low_stock": 2,
    "out_of_stock": 1,
    "pending_receipts": 3,
    "pending_deliveries": 2,
    "scheduled_transfers": 1
  },
  "low_stock_items": [
    { "product_id": 14, "sku": "BRG001", "name": "Bearing", "uom": "unit",
      "on_hand": "30.000", "min_qty": "50.000", "suggested_order": "470.000", "stock_status": "low" }
  ],
  "recent_operations": [ "OperationSummary", "..." ],
  "ledger_ok": true
}

pending_* / scheduled_transfers = operations of that type in draft, waiting or ready.

Method Path Access Query Returns
GET /api/dashboard/activity auth days (1–90, default 14), warehouse_id { days: [{ date, receipt, delivery, transfer, adjustment }], pipeline: { draft, waiting, ready } }: validated operations per day (UTC) and open operations by stage, for the dashboard charts

low_stock_items lists every product below its reorder point, out-of-stock first, then low; each carries suggested_order (up to max_qty). The dashboard's Reorder button turns one into a draft receipt, and Record count on a Count-next item creates a draft adjustment for that product-location.

Endpoint → owner → table map

Endpoint group Backend owner Frontend owner Main tables
auth M4 M4 users, password_resets
categories, products M1 M2 categories, products, stock_balances
warehouses, locations M1 M2 warehouses, locations
inventory M1 M2 / M3 (available) / M4 (integrity badge) stock_balances, stock_ledger
operations M1 M3 operations, operation_lines, operation_sequences, stock_balances, stock_ledger
ledger M1 M4 stock_ledger
dashboard M4 M4 all (read-only aggregates)