feat: add admin inventory management (Phase 1)

- Add admin API endpoints for inventory management
- Add inventory page with vendor selector and filtering
- Add admin schemas for cross-vendor inventory operations
- Support digital products with unlimited inventory
- Add integration tests for admin inventory API
- Add inventory management guide documentation

Mirrors vendor inventory functionality with admin-level access.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 21:05:12 +01:00
parent 0ab10128ae
commit 8d8d41808b
12 changed files with 2880 additions and 3 deletions

View File

@@ -0,0 +1,366 @@
# Inventory Management
## Overview
The Wizamart platform provides comprehensive inventory management with support for:
- **Multi-location tracking** - Track stock across warehouses, stores, and storage bins
- **Reservation system** - Reserve items for pending orders
- **Digital products** - Automatic unlimited inventory for digital goods
- **Admin operations** - Manage inventory on behalf of vendors
---
## Key Concepts
### Storage Locations
Inventory is tracked at the **storage location level**. Each product can have stock in multiple locations:
```
Product: "Wireless Headphones"
├── WAREHOUSE_MAIN: 100 units (10 reserved)
├── WAREHOUSE_WEST: 50 units (0 reserved)
└── STORE_FRONT: 25 units (5 reserved)
Total: 175 units | Reserved: 15 | Available: 160
```
**Location naming:** Locations are text strings, normalized to UPPERCASE (e.g., `WAREHOUSE_A`, `STORE_01`).
### Inventory States
| Field | Description |
|-------|-------------|
| `quantity` | Total physical stock at location |
| `reserved_quantity` | Items reserved for pending orders |
| `available_quantity` | `quantity - reserved_quantity` (can be sold) |
### Product Types & Inventory
| Product Type | Inventory Behavior |
|--------------|-------------------|
| **Physical** | Requires inventory tracking, orders check available stock |
| **Digital** | **Unlimited inventory** - no stock constraints |
| **Service** | Treated as digital (unlimited) |
| **Subscription** | Treated as digital (unlimited) |
---
## Digital Products
Digital products have **unlimited inventory** by default. This means:
- Orders for digital products never fail due to "insufficient inventory"
- No need to create inventory entries for digital products
- The `available_inventory` property returns `999999` (effectively unlimited)
### How It Works
```python
# In Product model
@property
def has_unlimited_inventory(self) -> bool:
"""Digital products have unlimited inventory."""
return self.is_digital
@property
def available_inventory(self) -> int:
"""Calculate available inventory."""
if self.has_unlimited_inventory:
return 999999 # Unlimited
return sum(inv.available_quantity for inv in self.inventory_entries)
```
### Setting a Product as Digital
Digital products are identified by the `is_digital` flag on the `MarketplaceProduct`:
```python
marketplace_product.is_digital = True
marketplace_product.product_type_enum = "digital"
marketplace_product.digital_delivery_method = "license_key" # or "download", "email"
```
---
## Inventory Operations
### Set Inventory
Replace the exact quantity at a location:
```http
POST /api/v1/vendor/inventory/set
{
"product_id": 123,
"location": "WAREHOUSE_A",
"quantity": 100
}
```
### Adjust Inventory
Add or remove stock (positive = add, negative = remove):
```http
POST /api/v1/vendor/inventory/adjust
{
"product_id": 123,
"location": "WAREHOUSE_A",
"quantity": -10 // Remove 10 units
}
```
### Reserve Inventory
Mark items as reserved for an order:
```http
POST /api/v1/vendor/inventory/reserve
{
"product_id": 123,
"location": "WAREHOUSE_A",
"quantity": 5
}
```
### Release Reservation
Cancel a reservation (order cancelled):
```http
POST /api/v1/vendor/inventory/release
{
"product_id": 123,
"location": "WAREHOUSE_A",
"quantity": 5
}
```
### Fulfill Reservation
Complete an order (items shipped):
```http
POST /api/v1/vendor/inventory/fulfill
{
"product_id": 123,
"location": "WAREHOUSE_A",
"quantity": 5
}
```
This decreases both `quantity` and `reserved_quantity`.
---
## Reservation Workflow
```
┌─────────────────┐
│ Order Created │
└────────┬────────┘
┌─────────────────┐
│ Reserve Items │ reserved_quantity += order_qty
└────────┬────────┘
┌────┴────┐
│ │
▼ ▼
┌───────┐ ┌──────────┐
│Cancel │ │ Ship │
└───┬───┘ └────┬─────┘
│ │
▼ ▼
┌─────────┐ ┌──────────────┐
│ Release │ │ Fulfill │
│reserved │ │ quantity -= │
│ -= qty │ │ reserved -= │
└─────────┘ └──────────────┘
```
---
## Admin Inventory Management
Administrators can manage inventory on behalf of any vendor through the admin UI at `/admin/inventory` or via the API.
### Admin UI Features
The admin inventory page provides:
- **Overview Statistics** - Total entries, stock quantities, reserved items, and low stock alerts
- **Filtering** - Filter by vendor, location, and low stock threshold
- **Search** - Search by product title or SKU
- **Stock Adjustment** - Add or remove stock with optional reason tracking
- **Set Quantity** - Set exact stock quantity at any location
- **Delete Entries** - Remove inventory entries
### Admin API Endpoints
### List All Inventory
```http
GET /api/v1/admin/inventory
GET /api/v1/admin/inventory?vendor_id=1
GET /api/v1/admin/inventory?low_stock=10
```
### Get Inventory Statistics
```http
GET /api/v1/admin/inventory/stats
Response:
{
"total_entries": 150,
"total_quantity": 5000,
"total_reserved": 200,
"total_available": 4800,
"low_stock_count": 12,
"vendors_with_inventory": 5,
"unique_locations": 8
}
```
### Low Stock Alerts
```http
GET /api/v1/admin/inventory/low-stock?threshold=10
Response:
[
{
"product_id": 123,
"vendor_name": "TechStore",
"product_title": "USB Cable",
"location": "WAREHOUSE_A",
"quantity": 3,
"available_quantity": 2
}
]
```
### Set Inventory (Admin)
```http
POST /api/v1/admin/inventory/set
{
"vendor_id": 1,
"product_id": 123,
"location": "WAREHOUSE_A",
"quantity": 100
}
```
### Adjust Inventory (Admin)
```http
POST /api/v1/admin/inventory/adjust
{
"vendor_id": 1,
"product_id": 123,
"location": "WAREHOUSE_A",
"quantity": 25,
"reason": "Restocking from supplier"
}
```
---
## Database Schema
### Inventory Table
```sql
CREATE TABLE inventory (
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES products(id),
vendor_id INTEGER NOT NULL REFERENCES vendors(id),
location VARCHAR NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0,
reserved_quantity INTEGER DEFAULT 0,
gtin VARCHAR,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(product_id, location)
);
CREATE INDEX idx_inventory_vendor_product ON inventory(vendor_id, product_id);
CREATE INDEX idx_inventory_product_location ON inventory(product_id, location);
```
### Constraints
- **Unique constraint:** `(product_id, location)` - One entry per product/location
- **Foreign keys:** References `products` and `vendors` tables
- **Non-negative:** `quantity` and `reserved_quantity` must be >= 0
---
## Best Practices
### Physical Products
1. **Create inventory entries** before accepting orders
2. **Use meaningful location names** (e.g., `WAREHOUSE_MAIN`, `STORE_NYC`)
3. **Monitor low stock** using the admin dashboard or API
4. **Reserve on order creation** to prevent overselling
### Digital Products
1. **No inventory setup needed** - unlimited by default
2. **Optional:** Create entries for license key tracking
3. **Focus on fulfillment** - digital delivery mechanism
### Multi-Location
1. **Aggregate queries** use `Product.total_inventory` and `Product.available_inventory`
2. **Location-specific** operations use the Inventory model directly
3. **Transfers** between locations: adjust down at source, adjust up at destination
---
## API Reference
### Vendor Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/v1/vendor/inventory/set` | Set exact quantity |
| POST | `/api/v1/vendor/inventory/adjust` | Add/remove quantity |
| POST | `/api/v1/vendor/inventory/reserve` | Reserve for order |
| POST | `/api/v1/vendor/inventory/release` | Cancel reservation |
| POST | `/api/v1/vendor/inventory/fulfill` | Complete order |
| GET | `/api/v1/vendor/inventory/product/{id}` | Product summary |
| GET | `/api/v1/vendor/inventory` | List with filters |
| PUT | `/api/v1/vendor/inventory/{id}` | Update entry |
| DELETE | `/api/v1/vendor/inventory/{id}` | Delete entry |
### Admin Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/admin/inventory` | List all (cross-vendor) |
| GET | `/api/v1/admin/inventory/stats` | Platform statistics |
| GET | `/api/v1/admin/inventory/low-stock` | Low stock alerts |
| GET | `/api/v1/admin/inventory/vendors` | Vendors with inventory |
| GET | `/api/v1/admin/inventory/locations` | Unique locations |
| GET | `/api/v1/admin/inventory/vendors/{id}` | Vendor inventory |
| GET | `/api/v1/admin/inventory/products/{id}` | Product summary |
| POST | `/api/v1/admin/inventory/set` | Set (requires vendor_id) |
| POST | `/api/v1/admin/inventory/adjust` | Adjust (requires vendor_id) |
| PUT | `/api/v1/admin/inventory/{id}` | Update entry |
| DELETE | `/api/v1/admin/inventory/{id}` | Delete entry |
---
## Related Documentation
- [Product Management](product-management.md)
- [Admin Inventory Migration Plan](../implementation/inventory-admin-migration.md)
- [Vendor Operations Expansion](../development/migration/vendor-operations-expansion.md)

View File

@@ -0,0 +1,370 @@
# Admin Inventory Management Migration Plan
## Overview
**Objective:** Add inventory management capabilities to the admin "Vendor Operations" section, allowing administrators to view and manage vendor inventory on their behalf.
**Status:** Phase 1 Complete
---
## Current State Analysis
### What Exists
The inventory system is **fully implemented at the vendor API level** with comprehensive functionality:
| Component | Status | Location |
|-----------|--------|----------|
| Database Model | ✅ Complete | `models/database/inventory.py` |
| Pydantic Schemas | ✅ Complete | `models/schema/inventory.py` |
| Service Layer | ✅ Complete | `app/services/inventory_service.py` |
| Vendor API | ✅ Complete | `app/api/v1/vendor/inventory.py` |
| Exceptions | ✅ Complete | `app/exceptions/inventory.py` |
| Unit Tests | ✅ Complete | `tests/unit/services/test_inventory_service.py` |
| Integration Tests | ✅ Complete | `tests/integration/api/v1/vendor/test_inventory.py` |
| Vendor UI | 🔲 Placeholder | `app/templates/vendor/inventory.html` |
| Admin API | 🔲 Not Started | - |
| Admin UI | 🔲 Not Started | - |
| Audit Trail | 🔲 Not Started | Logs only, no dedicated table |
### Storage/Location Architecture
The inventory system tracks stock at the **storage location level**:
```
Product A
├── WAREHOUSE_MAIN: 100 units (10 reserved)
├── WAREHOUSE_WEST: 50 units (0 reserved)
└── STORE_FRONT: 25 units (5 reserved)
Total: 175 units | Reserved: 15 | Available: 160
```
**Key design decisions:**
- One product can have inventory across multiple locations
- Unique constraint: `(product_id, location)` - one entry per product/location
- Locations are text strings, normalized to UPPERCASE
- Available quantity = `quantity - reserved_quantity`
### Existing Operations
| Operation | Description | Service Method |
|-----------|-------------|----------------|
| **Set** | Replace exact quantity at location | `set_inventory()` |
| **Adjust** | Add/remove quantity (positive/negative) | `adjust_inventory()` |
| **Reserve** | Mark items for pending order | `reserve_inventory()` |
| **Release** | Cancel reservation | `release_reservation()` |
| **Fulfill** | Complete order (reduces both qty & reserved) | `fulfill_reservation()` |
| **Get Product** | Summary across all locations | `get_product_inventory()` |
| **Get Vendor** | List with filters | `get_vendor_inventory()` |
| **Update** | Partial field update | `update_inventory()` |
| **Delete** | Remove inventory entry | `delete_inventory()` |
### Vendor API Endpoints
All endpoints at `/api/v1/vendor/inventory/*`:
| Method | Endpoint | Operation |
|--------|----------|-----------|
| POST | `/inventory/set` | Set exact quantity |
| POST | `/inventory/adjust` | Add/remove quantity |
| POST | `/inventory/reserve` | Reserve for order |
| POST | `/inventory/release` | Cancel reservation |
| POST | `/inventory/fulfill` | Complete order |
| GET | `/inventory/product/{id}` | Product summary |
| GET | `/inventory` | List with filters |
| PUT | `/inventory/{id}` | Update entry |
| DELETE | `/inventory/{id}` | Delete entry |
---
## Gap Analysis
### What's Missing for Admin Vendor Operations
1. **Admin API Endpoints** - ✅ Implemented in Phase 1
2. **Admin UI Page** - No inventory management interface in admin panel
3. **Vendor Selector** - Admin needs to select which vendor to manage
4. **Cross-Vendor View** - ✅ Implemented in Phase 1
5. **Audit Trail** - Only application logs, no queryable audit history
6. **Bulk Operations** - No bulk adjust/import capabilities
7. **Low Stock Alerts** - Basic filter exists, no alert configuration
### Digital Products - Infinite Inventory ✅
**Implementation Complete**
Digital products now have unlimited inventory by default:
```python
# In Product model (models/database/product.py)
UNLIMITED_INVENTORY = 999999
@property
def has_unlimited_inventory(self) -> bool:
"""Digital products have unlimited inventory."""
return self.is_digital
@property
def available_inventory(self) -> int:
"""Calculate available inventory (total - reserved).
Digital products return unlimited inventory.
"""
if self.has_unlimited_inventory:
return self.UNLIMITED_INVENTORY
return sum(inv.available_quantity for inv in self.inventory_entries)
```
**Behavior:**
- Physical products: Sum of inventory entries (0 if no entries)
- Digital products: Returns `999999` (unlimited) regardless of entries
- Orders for digital products never fail due to "insufficient inventory"
**Tests:** `tests/unit/models/database/test_product.py::TestProductInventoryProperties`
**Documentation:** [Inventory Management Guide](../guides/inventory-management.md)
---
## Migration Plan
### Phase 1: Admin API Endpoints
**Goal:** Expose inventory management to admin users with vendor selection
#### 1.1 New File: `app/api/v1/admin/inventory.py`
Admin endpoints that mirror vendor functionality with vendor_id as parameter:
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/admin/inventory` | List all inventory (cross-vendor) |
| GET | `/admin/inventory/vendors/{vendor_id}` | Vendor-specific inventory |
| GET | `/admin/inventory/products/{product_id}` | Product inventory summary |
| POST | `/admin/inventory/set` | Set inventory (requires vendor_id) |
| POST | `/admin/inventory/adjust` | Adjust inventory |
| PUT | `/admin/inventory/{id}` | Update inventory entry |
| DELETE | `/admin/inventory/{id}` | Delete inventory entry |
| GET | `/admin/inventory/low-stock` | Low stock report |
#### 1.2 Schema Extensions
Add admin-specific request schemas in `models/schema/inventory.py`:
```python
class AdminInventoryCreate(InventoryCreate):
"""Admin version - requires explicit vendor_id."""
vendor_id: int = Field(..., description="Target vendor ID")
class AdminInventoryAdjust(InventoryAdjust):
"""Admin version - requires explicit vendor_id."""
vendor_id: int = Field(..., description="Target vendor ID")
class AdminInventoryListResponse(BaseModel):
"""Cross-vendor inventory list."""
inventories: list[InventoryResponse]
total: int
skip: int
limit: int
vendor_filter: int | None = None
```
#### 1.3 Service Layer Reuse
The existing `InventoryService` already accepts `vendor_id` as a parameter - **no service changes needed**. Admin endpoints simply pass the vendor_id from the request instead of from the JWT token.
### Phase 2: Admin UI
**Goal:** Create admin inventory management page
#### 2.1 New Files
| File | Description |
|------|-------------|
| `app/templates/admin/inventory.html` | Main inventory page |
| `static/admin/js/inventory.js` | Alpine.js controller |
#### 2.2 UI Features
1. **Vendor Selector Dropdown** - Filter by vendor (or show all)
2. **Inventory Table** - Product, Location, Quantity, Reserved, Available
3. **Search/Filter** - By product name, location, low stock
4. **Adjust Modal** - Quick add/remove with reason
5. **Pagination** - Handle large inventories
6. **Export** - CSV download (future)
#### 2.3 Page Layout
```
┌─────────────────────────────────────────────────────────┐
│ Inventory Management [Vendor: All ▼] │
├─────────────────────────────────────────────────────────┤
│ [Search...] [Location ▼] [Low Stock Only ☐] │
├─────────────────────────────────────────────────────────┤
│ Product │ Location │ Qty │ Reserved │ Available │
│──────────────┼─────────────┼─────┼──────────┼───────────│
│ Widget A │ WAREHOUSE_A │ 100 │ 10 │ 90 [⚙️] │
│ Widget A │ WAREHOUSE_B │ 50 │ 0 │ 50 [⚙️] │
│ Gadget B │ WAREHOUSE_A │ 25 │ 5 │ 20 [⚙️] │
├─────────────────────────────────────────────────────────┤
│ Showing 1-20 of 150 [< 1 2 3 ... >] │
└─────────────────────────────────────────────────────────┘
```
#### 2.4 Sidebar Integration
Add to `app/templates/admin/partials/sidebar.html`:
```html
<!-- Under Vendor Operations section -->
<li class="relative px-6 py-3">
<a href="/admin/inventory" ...>
<span class="inline-flex items-center">
<!-- inventory icon -->
<span class="ml-4">Inventory</span>
</span>
</a>
</li>
```
### Phase 3: Audit Trail (Optional Enhancement)
**Goal:** Track inventory changes with queryable history
#### 3.1 Database Migration
```sql
CREATE TABLE inventory_audit_log (
id SERIAL PRIMARY KEY,
inventory_id INTEGER REFERENCES inventory(id) ON DELETE SET NULL,
product_id INTEGER NOT NULL,
vendor_id INTEGER NOT NULL,
location VARCHAR(255) NOT NULL,
-- Change details
operation VARCHAR(50) NOT NULL, -- 'set', 'adjust', 'reserve', 'release', 'fulfill'
quantity_before INTEGER,
quantity_after INTEGER,
reserved_before INTEGER,
reserved_after INTEGER,
adjustment_amount INTEGER,
-- Context
reason VARCHAR(500),
performed_by INTEGER REFERENCES users(id),
performed_by_type VARCHAR(20) NOT NULL, -- 'vendor', 'admin', 'system'
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_audit_vendor ON inventory_audit_log(vendor_id);
CREATE INDEX idx_audit_product ON inventory_audit_log(product_id);
CREATE INDEX idx_audit_created ON inventory_audit_log(created_at);
```
#### 3.2 Service Enhancement
Add audit logging to `InventoryService` methods:
```python
def _log_audit(
self,
db: Session,
inventory: Inventory,
operation: str,
qty_before: int,
qty_after: int,
reserved_before: int,
reserved_after: int,
user_id: int | None,
user_type: str,
reason: str | None = None
) -> None:
"""Record inventory change in audit log."""
audit = InventoryAuditLog(
inventory_id=inventory.id,
product_id=inventory.product_id,
vendor_id=inventory.vendor_id,
location=inventory.location,
operation=operation,
quantity_before=qty_before,
quantity_after=qty_after,
reserved_before=reserved_before,
reserved_after=reserved_after,
adjustment_amount=qty_after - qty_before,
reason=reason,
performed_by=user_id,
performed_by_type=user_type,
)
db.add(audit)
```
---
## Implementation Checklist
### Phase 1: Admin API ✅
- [x] Create `app/api/v1/admin/inventory.py`
- [x] Add admin inventory schemas to `models/schema/inventory.py`
- [x] Register routes in `app/api/v1/admin/__init__.py`
- [x] Write integration tests `tests/integration/api/v1/admin/test_inventory.py`
### Phase 2: Admin UI
- [ ] Create `app/templates/admin/inventory.html`
- [ ] Create `static/admin/js/inventory.js`
- [ ] Add route in `app/routes/admin.py`
- [ ] Add sidebar menu item
- [ ] Update `static/admin/js/init-alpine.js` for page mapping
### Phase 3: Audit Trail (Optional)
- [ ] Create Alembic migration for `inventory_audit_log` table
- [ ] Create `models/database/inventory_audit_log.py`
- [ ] Update `InventoryService` with audit logging
- [ ] Add audit history endpoint
- [ ] Add audit history UI component
---
## Testing Strategy
### Unit Tests
- Admin schema validation tests
- Audit log creation tests (if implemented)
### Integration Tests
- Admin inventory endpoints with authentication
- Vendor isolation verification (admin can access any vendor)
- Audit trail creation on operations
### Manual Testing
- Verify vendor selector works correctly
- Test adjust modal workflow
- Confirm pagination with large datasets
---
## Rollback Plan
Each phase is independent:
1. **Phase 1 Rollback:** Remove admin inventory routes from `__init__.py`
2. **Phase 2 Rollback:** Remove sidebar link, delete template/JS files
3. **Phase 3 Rollback:** Run down migration to drop audit table
---
## Dependencies
- Existing `InventoryService` (no changes required)
- Admin authentication (`get_current_admin_api`)
- Vendor model for vendor selector dropdown
---
## Related Documentation
- [Vendor Operations Expansion Plan](../development/migration/vendor-operations-expansion.md)
- [Admin Integration Guide](../backend/admin-integration-guide.md)
- [Architecture Patterns](../architecture/architecture-patterns.md)