diff --git a/.claude/hooks/post-tool-use.sh b/.claude/hooks/post-tool-use.sh
old mode 100755
new mode 100644
diff --git a/CLAUDE.md b/CLAUDE.md
index d2086efa..979d5f04 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,7 +1,11 @@
# CLAUDE.md
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
Factory Inventory Management System Demo with GitHub integration - Full-stack application with Vue 3 frontend, Python FastAPI backend, and in-memory mock data (no database).
+> Nested guidance: `client/CLAUDE.md` (Vue 3 patterns) and `server/CLAUDE.md` (FastAPI patterns) are auto-loaded when you work in those directories — consult them for subsystem detail.
+
## Critical Tool Usage Rules
### Subagents
@@ -38,20 +42,35 @@ uv run python main.py
# Frontend
cd client
npm install && npm run dev
+
+# Tests (backend, 51 tests via pytest + FastAPI TestClient)
+cd tests
+uv run pytest -v
+uv run pytest backend/test_inventory.py::TestInventoryEndpoints::test_get_all_inventory -v # single test
```
+On macOS/Linux, `./scripts/start.sh` and `./scripts/stop.sh` start/stop both servers. On Windows, run the manual commands above in separate terminals.
+
## Key Patterns
**Filter System**: 4 filters (Time Period, Warehouse, Category, Order Status) apply to all data via query params
**Data Flow**: Vue filters → `client/src/api.js` → FastAPI → In-memory filtering → Pydantic validation → Computed properties
**Reactivity**: Raw data in refs (`allOrders`, `inventoryItems`), derived data in computed properties
+**Routing**: `client/src/main.js` defines vue-router routes — `/` Dashboard, `/inventory`, `/orders`, `/demand`, `/spending`, `/reports`
+**Shared state (composables)**: `useFilters` (global filters), `useAuth` (auth/profile), `useI18n` (locale)
+**i18n**: English/Japanese locales in `client/src/locales/{en,ja}.js`, toggled via `LanguageSwitcher.vue`
+**Currency**: Format via `client/src/utils/currency.js` (don't hand-roll `toLocaleString` per component)
## API Endpoints
+All routes are defined in `server/main.py`. Shared helpers: `apply_filters()` (warehouse/category/status) and `filter_by_month()` (month `2025-01` or quarter `Q1-2025` via `QUARTER_MAP`).
- `GET /api/inventory` - Filters: warehouse, category
+- `GET /api/inventory/{item_id}` - Single item (404 if missing)
- `GET /api/orders` - Filters: warehouse, category, status, month
+- `GET /api/orders/{order_id}` - Single order (404 if missing)
- `GET /api/dashboard/summary` - All filters
-- `GET /api/demand`, `/api/backlog` - No filters
-- `GET /api/spending/*` - Summary, monthly, categories, transactions
+- `GET /api/demand`, `/api/backlog` - No filters (backlog injects `has_purchase_order` flag)
+- `GET /api/spending/*` - summary, monthly, categories, transactions
+- `GET /api/reports/quarterly`, `/api/reports/monthly-trends` - Computed from orders
## Common Issues
1. Use unique keys in v-for (not `index`) - use `sku`, `month`, etc.
@@ -59,6 +78,10 @@ npm install && npm run dev
3. Update Pydantic models when changing JSON data structure
4. Inventory filters don't support month (no time dimension)
5. Revenue goals: $800K/month single, $9.6M YTD all months
+6. `client/src/api.js` calls `/api/tasks` (CRUD) and `/api/purchase-orders` (POST/GET) endpoints that are **not implemented** in `server/main.py` — add the backend routes before wiring up those features
+
+## Conventions
+- Always document non-obvious logic changes with comments
## File Locations
- Views: `client/src/views/*.vue`
diff --git a/client/src/App.vue b/client/src/App.vue
index c2da05a5..dd6849b1 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -22,6 +22,9 @@
{{ t('nav.demandForecast') }}
+
+ {{ t('nav.restocking') }}
+
Reports
diff --git a/client/src/api.js b/client/src/api.js
index 11cb9db7..87304b78 100644
--- a/client/src/api.js
+++ b/client/src/api.js
@@ -33,6 +33,11 @@ export const api = {
return response.data
},
+ async placeRestockOrder(payload) {
+ const response = await axios.post(`${API_BASE_URL}/restock-orders`, payload)
+ return response.data
+ },
+
async getDemandForecasts() {
const response = await axios.get(`${API_BASE_URL}/demand`)
return response.data
diff --git a/client/src/locales/en.js b/client/src/locales/en.js
index 03a58fe6..67430a63 100644
--- a/client/src/locales/en.js
+++ b/client/src/locales/en.js
@@ -6,6 +6,7 @@ export default {
orders: 'Orders',
finance: 'Finance',
demandForecast: 'Demand Forecast',
+ restocking: 'Restocking',
companyName: 'Catalyst Components',
subtitle: 'Inventory Management System'
},
@@ -204,11 +205,37 @@ export default {
shipped: 'Shipped',
processing: 'Processing',
backordered: 'Backordered',
+ submitted: 'Submitted',
inStock: 'In Stock',
lowStock: 'Low Stock',
adequate: 'Adequate'
},
+ // Restocking
+ restocking: {
+ title: 'Restocking',
+ description: 'Set a budget and get restock recommendations from demand forecasts',
+ budgetLabel: 'Available Budget',
+ recommendations: 'Recommended Restock Items',
+ runningTotal: 'Order Total',
+ remaining: 'Remaining Budget',
+ placeOrder: 'Place Order',
+ placing: 'Placing Order...',
+ orderPlaced: 'Order {number} submitted successfully',
+ noRecommendations: 'No items fit within the selected budget',
+ submittedOrders: 'Submitted Orders',
+ leadTime: 'Lead Time',
+ leadTimeDays: '{days} days',
+ table: {
+ sku: 'SKU',
+ itemName: 'Item Name',
+ trend: 'Trend',
+ recommendedQty: 'Recommended Qty',
+ estUnitCost: 'Est. Unit Cost',
+ lineTotal: 'Line Total'
+ }
+ },
+
// Trends
trends: {
increasing: 'increasing',
diff --git a/client/src/locales/ja.js b/client/src/locales/ja.js
index db33223a..0d2398d5 100644
--- a/client/src/locales/ja.js
+++ b/client/src/locales/ja.js
@@ -6,6 +6,7 @@ export default {
orders: '注文',
finance: '財務',
demandForecast: '需要予測',
+ restocking: '補充',
companyName: '触媒コンポーネンツ',
subtitle: '在庫管理システム'
},
@@ -204,11 +205,37 @@ export default {
shipped: '出荷済み',
processing: '処理中',
backordered: 'バックオーダー',
+ submitted: '送信済み',
inStock: '在庫あり',
lowStock: '在庫僅少',
adequate: '適量'
},
+ // Restocking
+ restocking: {
+ title: '補充',
+ description: '予算を設定し、需要予測から補充推奨を取得します',
+ budgetLabel: '利用可能な予算',
+ recommendations: '推奨補充品目',
+ runningTotal: '注文合計',
+ remaining: '残りの予算',
+ placeOrder: '注文する',
+ placing: '注文中...',
+ orderPlaced: '注文 {number} が正常に送信されました',
+ noRecommendations: '選択した予算内に収まる品目がありません',
+ submittedOrders: '送信済み注文',
+ leadTime: 'リードタイム',
+ leadTimeDays: '{days}日間',
+ table: {
+ sku: 'SKU',
+ itemName: '品目名',
+ trend: 'トレンド',
+ recommendedQty: '推奨数量',
+ estUnitCost: '推定単価',
+ lineTotal: '小計'
+ }
+ },
+
// Trends
trends: {
increasing: '増加',
diff --git a/client/src/main.js b/client/src/main.js
index 477c2d96..3347ae01 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -7,6 +7,7 @@ import Orders from './views/Orders.vue'
import Demand from './views/Demand.vue'
import Spending from './views/Spending.vue'
import Reports from './views/Reports.vue'
+import Restocking from './views/Restocking.vue'
const router = createRouter({
history: createWebHistory(),
@@ -15,6 +16,7 @@ const router = createRouter({
{ path: '/inventory', component: Inventory },
{ path: '/orders', component: Orders },
{ path: '/demand', component: Demand },
+ { path: '/restocking', component: Restocking },
{ path: '/spending', component: Spending },
{ path: '/reports', component: Reports }
]
diff --git a/client/src/views/Orders.vue b/client/src/views/Orders.vue
index 7413f6e6..e394ea0b 100644
--- a/client/src/views/Orders.vue
+++ b/client/src/views/Orders.vue
@@ -25,6 +25,52 @@
{{ t('status.backordered') }}
{{ getOrdersByStatus('Backordered').length }}
+
+
{{ t('status.submitted') }}
+
{{ submittedOrders.length }}
+
+
+
+
+
+
+
+
+
+ {{ t('orders.table.orderNumber') }}
+ {{ t('orders.table.items') }}
+ {{ t('orders.table.orderDate') }}
+ {{ t('orders.table.expectedDelivery') }}
+ {{ t('restocking.leadTime') }}
+ {{ t('orders.table.totalValue') }}
+
+
+
+
+ {{ order.order_number }}
+
+
+
+ {{ t('orders.itemsCount', { count: order.items.length }) }}
+
+
+
+ {{ translateProductName(item.name) }}
+ {{ t('orders.quantity') }}: {{ item.quantity }} @ {{ currencySymbol }}{{ item.unit_price }}
+
+
+
+
+ {{ formatDate(order.order_date) }}
+ {{ formatDate(order.expected_delivery) }}
+ {{ t('restocking.leadTimeDays', { days: leadTimeDays(order) }) }}
+ {{ currencySymbol }}{{ order.total_value.toLocaleString() }}
+
+
+
+
@@ -95,6 +141,7 @@ export default {
const loading = ref(true)
const error = ref(null)
const orders = ref([])
+ const submittedOrders = ref([])
// Use shared filters
const {
@@ -109,7 +156,10 @@ export default {
try {
loading.value = true
const filters = getCurrentFilters()
- const fetchedOrders = await api.getOrders(filters)
+ const [fetchedOrders, submitted] = await Promise.all([
+ api.getOrders(filters),
+ api.getOrders({ status: 'submitted' })
+ ])
// Sort orders by order_date (earliest first)
orders.value = fetchedOrders.sort((a, b) => {
@@ -117,6 +167,9 @@ export default {
const dateB = new Date(b.order_date)
return dateA - dateB
})
+
+ // Sort submitted orders newest first
+ submittedOrders.value = submitted.sort((a, b) => new Date(b.order_date) - new Date(a.order_date))
} catch (err) {
error.value = 'Failed to load orders: ' + err.message
} finally {
@@ -138,11 +191,14 @@ export default {
'Delivered': 'success',
'Shipped': 'info',
'Processing': 'warning',
- 'Backordered': 'danger'
+ 'Backordered': 'danger',
+ 'Submitted': 'info'
}
return statusMap[status] || 'info'
}
+ const leadTimeDays = (o) => Math.round((new Date(o.expected_delivery) - new Date(o.order_date)) / 86400000)
+
const formatDate = (dateString) => {
const { currentLocale } = useI18n()
const locale = currentLocale.value === 'ja' ? 'ja-JP' : 'en-US'
@@ -160,8 +216,10 @@ export default {
loading,
error,
orders,
+ submittedOrders,
getOrdersByStatus,
getOrderStatusClass,
+ leadTimeDays,
formatDate,
currencySymbol,
translateProductName,
@@ -199,6 +257,10 @@ export default {
width: 140px;
}
+.col-leadtime {
+ width: 110px;
+}
+
.col-value {
width: 120px;
}
diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue
new file mode 100644
index 00000000..a33d63d0
--- /dev/null
+++ b/client/src/views/Restocking.vue
@@ -0,0 +1,347 @@
+
+
+
+
+
{{ t('common.loading') }}
+
{{ error }}
+
+
+
+
+
+
+
+ {{ formatCurrency(0, currentCurrency.value) }}
+ {{ formatCurrency(maxBudget, currentCurrency.value) }}
+
+
+
+
+
+
{{ successMessage }}
+
+
+
{{ submitError }}
+
+
+
+
+
+
+
+
+ {{ t('restocking.table.sku') }}
+ {{ t('restocking.table.itemName') }}
+ {{ t('restocking.table.trend') }}
+ {{ t('restocking.table.recommendedQty') }}
+ {{ t('restocking.table.estUnitCost') }}
+ {{ t('restocking.table.lineTotal') }}
+
+
+
+
+ {{ t('restocking.noRecommendations') }}
+
+
+ {{ item.item_sku }}
+ {{ translateProductName(item.item_name) }}
+
+ {{ t(`trends.${item.trend}`) }}
+
+ {{ item.qty }}
+ {{ formatCurrency(item.unitCost, currentCurrency.value) }}
+ {{ formatCurrency(item.lineTotal, currentCurrency.value) }}
+
+
+
+
+
+
+ {{ t('restocking.runningTotal') }}: {{ formatCurrency(runningTotal, currentCurrency.value) }}
+ {{ t('restocking.remaining') }}: {{ formatCurrency(remainingBudget, currentCurrency.value) }}
+
+
+
+
+ {{ submitting ? t('restocking.placing') : t('restocking.placeOrder') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/architecture.html b/docs/architecture.html
new file mode 100644
index 00000000..b5f575b9
--- /dev/null
+++ b/docs/architecture.html
@@ -0,0 +1,295 @@
+
+
+
+
+
+
Architecture · Factory Inventory Management System
+
+
+
+
+
+
+ System Architecture
+ Factory Inventory Management System
+ A full-stack demo application for inventory tracking, order management, demand forecasting, and spending analytics. A Vue 3 single-page app talks to a Python FastAPI service that serves filtered, validated data from in-memory JSON datasets — no database.
+
+ Vue 3 + Vite
+ Python FastAPI
+ In-memory JSON data
+ REST / JSON over HTTP
+ English / Japanese i18n
+
+
+
+
+
+ System Architecture — Three Tiers
+
+
+
+
+ Presentation — Browser
+ Vue 3 Composition API · Vite · vue-router · localhost:3000
+
+
+
ViewsDashboard, Inventory, Orders, Demand, Spending, Reports
+
ComponentsFilterBar, detail modals, ProfileMenu, LanguageSwitcher
+
ComposablesuseFilters, useAuth, useI18n (shared singleton state)
+
API clientsrc/api.js (axios)
+
+
+
+
+ ⇅
+ HTTP GET /api/* · query-param filters · JSON
+
+
+
+
+ Application — API Service
+ Python FastAPI · Uvicorn · CORS (*) · localhost:8001
+
+
+
Route handlersserver/main.py — /api/inventory, /orders, /dashboard, /spending, /reports
+
Filter helpersapply_filters() · filter_by_month() + QUARTER_MAP
+
Pydantic modelsResponse validation & typing
+
+
+
+
+ ⇅
+ module import at startup · loaded once into memory
+
+
+
+
+ Data — In-Memory Mock Store
+ server/mock_data.py · server/data/*.json
+
+
+
JSON datasetsinventory, orders, demand, backlog, spending, transactions, purchase_orders
+
Loadermock_data.py reads files into Python lists/dicts
+
LifecycleRead-only at runtime; restart reloads from disk
+
+
+
+
+
+
+
+
+ Technology Stack
+
+
+
Frontend
+
+ Vue 3 — Composition API throughout
+ Vite 5 — dev server & build (port 3000)
+ vue-router 4 — 6 client routes
+ axios — HTTP client in api.js
+ Custom SVG charts & CSS Grid layouts
+
+
+
+
Backend
+
+ FastAPI — REST endpoints
+ Uvicorn — ASGI server (port 8001)
+ Pydantic v2 — response models
+ CORS — open during development
+ Auto API docs at /docs
+
+
+
+
Data & Tooling
+
+ JSON files — no database
+ uv — Python env & runner
+ pytest + TestClient — 51 backend tests
+ npm — frontend package manager
+ i18n locales: en.js, ja.js
+
+
+
+
+
+
+
+ Request Data Flow
+
+
+
User filters
+
FilterBar updates shared refs in useFilters (Period, Warehouse, Category, Status).
+
+
→
+
+
API call
+
api.js builds query params and issues an axios GET to FastAPI.
+
+
→
+
+
Server filtering
+
apply_filters() and filter_by_month() narrow the in-memory lists.
+
+
→
+
+
Validation
+
Pydantic response_model validates and serializes the payload.
+
+
→
+
+
Reactive render
+
Raw data lands in refs; computed properties derive views & charts.
+
+
+
+
+
+
+ API Endpoints
+
+
+ Method Path Filters / Notes
+
+
+ GET /api/inventorywarehouse, category
+ GET /api/inventory/{item_id}single item — 404 if missing
+ GET /api/orderswarehouse, category, status, month/quarter
+ GET /api/orders/{order_id}single order — 404 if missing
+ GET /api/dashboard/summaryall four filters — aggregate metrics
+ GET /api/demandno filters
+ GET /api/backloginjects has_purchase_order flag
+ GET /api/spending/{summary|monthly|categories|transactions}no filters
+ GET /api/reports/{quarterly|monthly-trends}computed from orders
+
+
+
+
+
+
+ Mock Data Layer
+
+
+
32
inventory items inventory.json
+
56
transactions transactions.json
+
9
demand forecasts demand_forecasts.json
+
4
backlog items backlog_items.json
+
3
spending sections spending.json
+
+
+
+
+ Generated architecture overview for the Factory Inventory Management System demo. Frontend localhost:3000 · API localhost:8001 · interactive API docs at /docs . Demo only — no database, authentication, or production hardening.
+
+
+
+
+
diff --git a/scripts/start.sh b/scripts/start.sh
old mode 100755
new mode 100644
diff --git a/scripts/stop.sh b/scripts/stop.sh
old mode 100755
new mode 100644
diff --git a/server/main.py b/server/main.py
index a0c2d8c5..28db5674 100644
--- a/server/main.py
+++ b/server/main.py
@@ -2,6 +2,8 @@
from fastapi.middleware.cors import CORSMiddleware
from typing import List, Optional
from pydantic import BaseModel
+from datetime import datetime, timedelta
+from uuid import uuid4
from mock_data import inventory_items, orders, demand_forecasts, backlog_items, spending_summary, monthly_spending, category_spending, recent_transactions, purchase_orders
app = FastAPI(title="Factory Inventory Management System")
@@ -120,6 +122,16 @@ class CreatePurchaseOrderRequest(BaseModel):
expected_delivery_date: str
notes: Optional[str] = None
+class RestockOrderItem(BaseModel):
+ item_sku: str
+ item_name: str
+ quantity: int
+ unit_price: float
+
+class CreateRestockOrderRequest(BaseModel):
+ budget: float
+ items: List[RestockOrderItem]
+
# API endpoints
@app.get("/")
def root():
@@ -161,6 +173,59 @@ def get_order(order_id: str):
raise HTTPException(status_code=404, detail="Order not found")
return order
+@app.post("/api/restock-orders", response_model=Order)
+def create_restock_order(request: CreateRestockOrderRequest):
+ """Submit a restocking order; appends a 'Submitted' order to the in-memory orders list.
+
+ Note: this is a demo store - submitted orders live in memory only and reset on
+ server restart. Lead time is a fixed 14 days (expected_delivery = order_date + 14d).
+ """
+ if not request.items:
+ raise HTTPException(status_code=400, detail="No items to order")
+
+ now = datetime.now()
+ expected = now + timedelta(days=14)
+
+ # Generate the next order number from the existing ORD-2025-NNNN sequence
+ max_num = 0
+ for o in orders:
+ number = o.get("order_number", "")
+ if number.startswith("ORD-2025-"):
+ try:
+ max_num = max(max_num, int(number.split("-")[-1]))
+ except ValueError:
+ pass
+ next_number = f"ORD-2025-{max_num + 1:04d}"
+
+ # Map restock items (item_sku/item_name) to the existing order-item dict shape
+ items = [
+ {
+ "sku": item.item_sku,
+ "name": item.item_name,
+ "quantity": item.quantity,
+ "unit_price": round(item.unit_price, 2),
+ }
+ for item in request.items
+ ]
+ # Compute total server-side rather than trusting the client
+ total_value = round(sum(i["quantity"] * i["unit_price"] for i in items), 2)
+
+ new_order = {
+ "id": f"restock-{uuid4().hex[:8]}",
+ "order_number": next_number,
+ "customer": "Internal Restock",
+ "items": items,
+ "status": "Submitted",
+ "order_date": now.strftime("%Y-%m-%dT%H:%M:%S"),
+ "expected_delivery": expected.strftime("%Y-%m-%dT%H:%M:%S"),
+ "total_value": total_value,
+ "actual_delivery": None,
+ "warehouse": None,
+ "category": None,
+ }
+ orders.append(new_order)
+ return new_order
+
@app.get("/api/demand", response_model=List[DemandForecast])
def get_demand_forecasts():
"""Get demand forecasts"""
diff --git a/tests/backend/test_restock_orders.py b/tests/backend/test_restock_orders.py
new file mode 100644
index 00000000..71ce413b
--- /dev/null
+++ b/tests/backend/test_restock_orders.py
@@ -0,0 +1,125 @@
+"""
+Tests for the restock-orders API endpoint (POST /api/restock-orders).
+"""
+from datetime import datetime
+
+import pytest
+
+
+class TestRestockOrdersEndpoint:
+ """Test suite for submitting restocking orders."""
+
+ def _sample_payload(self):
+ """A valid restock order request payload."""
+ return {
+ "budget": 50000,
+ "items": [
+ {
+ "item_sku": "WDG-001",
+ "item_name": "Industrial Widget Type A",
+ "quantity": 150,
+ "unit_price": 42.5,
+ },
+ {
+ "item_sku": "GSK-203",
+ "item_name": "High-Temperature Gasket",
+ "quantity": 100,
+ "unit_price": 12.0,
+ },
+ ],
+ }
+
+ def test_place_restock_order_success(self, client):
+ """Test submitting a valid restock order returns a Submitted order."""
+ response = client.post("/api/restock-orders", json=self._sample_payload())
+ assert response.status_code == 200
+
+ order = response.json()
+ # Core order shape (matches the Order model)
+ for field in (
+ "id",
+ "order_number",
+ "customer",
+ "items",
+ "status",
+ "order_date",
+ "expected_delivery",
+ "total_value",
+ ):
+ assert field in order
+
+ assert order["status"] == "Submitted"
+ assert order["customer"] == "Internal Restock"
+ assert order["order_number"].startswith("ORD-2025-")
+
+ def test_restock_order_items_mapped_to_order_shape(self, client):
+ """Items must be stored in the order-item dict shape (sku/name/...)."""
+ response = client.post("/api/restock-orders", json=self._sample_payload())
+ order = response.json()
+
+ assert len(order["items"]) == 2
+ for item in order["items"]:
+ # Stored as sku/name, NOT item_sku/item_name
+ assert "sku" in item
+ assert "name" in item
+ assert "quantity" in item
+ assert "unit_price" in item
+ assert "item_sku" not in item
+ assert "item_name" not in item
+
+ def test_restock_order_total_value_calculation(self, client):
+ """total_value should be computed server-side from items."""
+ payload = self._sample_payload()
+ response = client.post("/api/restock-orders", json=payload)
+ order = response.json()
+
+ expected = sum(i["quantity"] * i["unit_price"] for i in payload["items"])
+ assert abs(order["total_value"] - expected) < 0.01
+
+ def test_restock_order_lead_time_is_14_days(self, client):
+ """expected_delivery should be exactly 14 days after order_date."""
+ response = client.post("/api/restock-orders", json=self._sample_payload())
+ order = response.json()
+
+ order_date = datetime.fromisoformat(order["order_date"])
+ expected_delivery = datetime.fromisoformat(order["expected_delivery"])
+ assert (expected_delivery - order_date).days == 14
+
+ def test_restock_order_appears_in_submitted_filter(self, client):
+ """A placed order is retrievable via GET /api/orders?status=submitted."""
+ response = client.post("/api/restock-orders", json=self._sample_payload())
+ placed = response.json()
+
+ listing = client.get("/api/orders?status=submitted")
+ assert listing.status_code == 200
+ data = listing.json()
+ numbers = [o["order_number"] for o in data]
+ assert placed["order_number"] in numbers
+ for o in data:
+ assert o["status"].lower() == "submitted"
+
+ def test_restock_order_number_is_unique_and_increments(self, client):
+ """Two successive orders get distinct, sequential order numbers."""
+ first = client.post("/api/restock-orders", json=self._sample_payload()).json()
+ second = client.post("/api/restock-orders", json=self._sample_payload()).json()
+
+ assert first["order_number"] != second["order_number"]
+ first_num = int(first["order_number"].split("-")[-1])
+ second_num = int(second["order_number"].split("-")[-1])
+ assert second_num == first_num + 1
+
+ def test_restock_order_empty_items_rejected(self, client):
+ """Submitting with no items returns 400."""
+ response = client.post(
+ "/api/restock-orders", json={"budget": 1000, "items": []}
+ )
+ assert response.status_code == 400
+ assert "detail" in response.json()
+
+ def test_restock_order_missing_fields_validation(self, client):
+ """Malformed item (missing required fields) returns 422."""
+ response = client.post(
+ "/api/restock-orders",
+ json={"budget": 1000, "items": [{"item_sku": "WDG-001"}]},
+ )
+ assert response.status_code == 422