refactor: migrate remaining routes to modules and enforce auto-discovery
MIGRATION: - Delete app/api/v1/vendor/analytics.py (duplicate - analytics module already auto-discovered) - Move usage routes from app/api/v1/vendor/usage.py to billing module - Move onboarding routes from app/api/v1/vendor/onboarding.py to marketplace module - Move features routes to billing module (admin + vendor) - Move inventory routes to inventory module (admin + vendor) - Move marketplace/letzshop routes to marketplace module - Move orders routes to orders module - Delete legacy letzshop service files (moved to marketplace module) DOCUMENTATION: - Add docs/development/migration/module-autodiscovery-migration.md with full migration history - Update docs/architecture/module-system.md with Entity Auto-Discovery Reference section - Add detailed sections for each entity type: routes, services, models, schemas, tasks, exceptions, templates, static files, locales, configuration ARCHITECTURE VALIDATION: - Add MOD-016: Routes must be in modules, not app/api/v1/ - Add MOD-017: Services must be in modules, not app/services/ - Add MOD-018: Tasks must be in modules, not app/tasks/ - Add MOD-019: Schemas must be in modules, not models/schema/ - Update scripts/validate_architecture.py with _validate_legacy_locations method - Update .architecture-rules/module.yaml with legacy location rules These rules enforce that all entities must be in self-contained modules. Legacy locations now trigger ERROR severity violations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,23 +6,20 @@ This module provides functions to register inventory routes
|
||||
with module-based access control.
|
||||
|
||||
NOTE: Routers are NOT auto-imported to avoid circular dependencies.
|
||||
Import directly from admin.py or vendor.py as needed:
|
||||
from app.modules.inventory.routes.admin import admin_router
|
||||
from app.modules.inventory.routes.vendor import vendor_router
|
||||
Import directly from api submodule as needed:
|
||||
from app.modules.inventory.routes.api import admin_router
|
||||
from app.modules.inventory.routes.api import vendor_router
|
||||
"""
|
||||
|
||||
# Routers are imported on-demand to avoid circular dependencies
|
||||
# Do NOT add auto-imports here
|
||||
|
||||
__all__ = ["admin_router", "vendor_router"]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy import routers to avoid circular dependencies."""
|
||||
if name == "admin_router":
|
||||
from app.modules.inventory.routes.admin import admin_router
|
||||
from app.modules.inventory.routes.api import admin_router
|
||||
return admin_router
|
||||
elif name == "vendor_router":
|
||||
from app.modules.inventory.routes.vendor import vendor_router
|
||||
from app.modules.inventory.routes.api import vendor_router
|
||||
return vendor_router
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# app/modules/inventory/routes/admin.py
|
||||
"""
|
||||
Inventory module admin routes.
|
||||
|
||||
This module wraps the existing admin inventory routes and adds
|
||||
module-based access control. Routes are re-exported from the
|
||||
original location with the module access dependency.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.deps import require_module_access
|
||||
|
||||
# Import original router (direct import to avoid circular dependency)
|
||||
from app.api.v1.admin.inventory import router as original_router
|
||||
|
||||
# Create module-aware router
|
||||
admin_router = APIRouter(
|
||||
prefix="/inventory",
|
||||
dependencies=[Depends(require_module_access("inventory"))],
|
||||
)
|
||||
|
||||
# Re-export all routes from the original module with module access control
|
||||
# The routes are copied to maintain the same API structure
|
||||
for route in original_router.routes:
|
||||
admin_router.routes.append(route)
|
||||
@@ -1,4 +1,21 @@
|
||||
# Routes will be migrated here from legacy locations
|
||||
# TODO: Move actual route implementations from app/api/v1/
|
||||
# app/modules/inventory/routes/api/__init__.py
|
||||
"""
|
||||
Inventory module API routes.
|
||||
|
||||
__all__ = []
|
||||
Provides REST API endpoints for inventory management:
|
||||
- Admin API: Platform-wide inventory management
|
||||
- Vendor API: Vendor-specific inventory operations
|
||||
"""
|
||||
|
||||
__all__ = ["admin_router", "vendor_router"]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy import routers to avoid circular dependencies."""
|
||||
if name == "admin_router":
|
||||
from app.modules.inventory.routes.api.admin import admin_router
|
||||
return admin_router
|
||||
elif name == "vendor_router":
|
||||
from app.modules.inventory.routes.api.vendor import vendor_router
|
||||
return vendor_router
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
436
app/modules/inventory/routes/api/admin.py
Normal file
436
app/modules/inventory/routes/api/admin.py
Normal file
@@ -0,0 +1,436 @@
|
||||
# app/modules/inventory/routes/api/admin.py
|
||||
"""
|
||||
Admin inventory management endpoints.
|
||||
|
||||
Provides inventory management capabilities for administrators:
|
||||
- View inventory across all vendors
|
||||
- View vendor-specific inventory
|
||||
- Set/adjust inventory on behalf of vendors
|
||||
- Low stock alerts and reporting
|
||||
|
||||
Admin Context: Uses admin JWT authentication.
|
||||
Vendor selection is passed as a request parameter.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.services.inventory_import_service import inventory_import_service
|
||||
from app.services.inventory_service import inventory_service
|
||||
from app.services.inventory_transaction_service import inventory_transaction_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.inventory.schemas import (
|
||||
AdminInventoryAdjust,
|
||||
AdminInventoryCreate,
|
||||
AdminInventoryListResponse,
|
||||
AdminInventoryLocationsResponse,
|
||||
AdminInventoryStats,
|
||||
AdminInventoryTransactionItem,
|
||||
AdminInventoryTransactionListResponse,
|
||||
AdminLowStockItem,
|
||||
AdminTransactionStatsResponse,
|
||||
AdminVendorsWithInventoryResponse,
|
||||
InventoryAdjust,
|
||||
InventoryCreate,
|
||||
InventoryMessageResponse,
|
||||
InventoryResponse,
|
||||
InventoryUpdate,
|
||||
ProductInventorySummary,
|
||||
)
|
||||
|
||||
admin_router = APIRouter(
|
||||
prefix="/inventory",
|
||||
dependencies=[Depends(require_module_access("inventory"))],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# List & Statistics Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.get("", response_model=AdminInventoryListResponse)
|
||||
def get_all_inventory(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
vendor_id: int | None = Query(None, description="Filter by vendor"),
|
||||
location: str | None = Query(None, description="Filter by location"),
|
||||
low_stock: int | None = Query(None, ge=0, description="Filter items below threshold"),
|
||||
search: str | None = Query(None, description="Search by product title or SKU"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get inventory across all vendors with filtering.
|
||||
|
||||
Allows admins to view and filter inventory across the platform.
|
||||
"""
|
||||
return inventory_service.get_all_inventory_admin(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
vendor_id=vendor_id,
|
||||
location=location,
|
||||
low_stock=low_stock,
|
||||
search=search,
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/stats", response_model=AdminInventoryStats)
|
||||
def get_inventory_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get platform-wide inventory statistics."""
|
||||
return inventory_service.get_inventory_stats_admin(db)
|
||||
|
||||
|
||||
@admin_router.get("/low-stock", response_model=list[AdminLowStockItem])
|
||||
def get_low_stock_items(
|
||||
threshold: int = Query(10, ge=0, description="Stock threshold"),
|
||||
vendor_id: int | None = Query(None, description="Filter by vendor"),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get items with low stock levels."""
|
||||
return inventory_service.get_low_stock_items_admin(
|
||||
db=db,
|
||||
threshold=threshold,
|
||||
vendor_id=vendor_id,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/vendors", response_model=AdminVendorsWithInventoryResponse)
|
||||
def get_vendors_with_inventory(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get list of vendors that have inventory entries."""
|
||||
return inventory_service.get_vendors_with_inventory_admin(db)
|
||||
|
||||
|
||||
@admin_router.get("/locations", response_model=AdminInventoryLocationsResponse)
|
||||
def get_inventory_locations(
|
||||
vendor_id: int | None = Query(None, description="Filter by vendor"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get list of unique inventory locations."""
|
||||
return inventory_service.get_inventory_locations_admin(db, vendor_id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Vendor-Specific Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.get("/vendors/{vendor_id}", response_model=AdminInventoryListResponse)
|
||||
def get_vendor_inventory(
|
||||
vendor_id: int,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
location: str | None = Query(None, description="Filter by location"),
|
||||
low_stock: int | None = Query(None, ge=0, description="Filter items below threshold"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get inventory for a specific vendor."""
|
||||
return inventory_service.get_vendor_inventory_admin(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
location=location,
|
||||
low_stock=low_stock,
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/products/{product_id}", response_model=ProductInventorySummary)
|
||||
def get_product_inventory(
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get inventory summary for a specific product across all locations."""
|
||||
return inventory_service.get_product_inventory_admin(db, product_id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Inventory Modification Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.post("/set", response_model=InventoryResponse)
|
||||
def set_inventory(
|
||||
inventory_data: AdminInventoryCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Set exact inventory quantity for a product at a location.
|
||||
|
||||
Admin version - requires explicit vendor_id in request body.
|
||||
"""
|
||||
# Verify vendor exists
|
||||
inventory_service.verify_vendor_exists(db, inventory_data.vendor_id)
|
||||
|
||||
# Convert to standard schema for service
|
||||
service_data = InventoryCreate(
|
||||
product_id=inventory_data.product_id,
|
||||
location=inventory_data.location,
|
||||
quantity=inventory_data.quantity,
|
||||
)
|
||||
|
||||
result = inventory_service.set_inventory(
|
||||
db=db,
|
||||
vendor_id=inventory_data.vendor_id,
|
||||
inventory_data=service_data,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Admin {current_admin.email} set inventory for product {inventory_data.product_id} "
|
||||
f"at {inventory_data.location}: {inventory_data.quantity} units"
|
||||
)
|
||||
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@admin_router.post("/adjust", response_model=InventoryResponse)
|
||||
def adjust_inventory(
|
||||
adjustment: AdminInventoryAdjust,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Adjust inventory by adding or removing quantity.
|
||||
|
||||
Positive quantity = add stock, negative = remove stock.
|
||||
Admin version - requires explicit vendor_id in request body.
|
||||
"""
|
||||
# Verify vendor exists
|
||||
inventory_service.verify_vendor_exists(db, adjustment.vendor_id)
|
||||
|
||||
# Convert to standard schema for service
|
||||
service_data = InventoryAdjust(
|
||||
product_id=adjustment.product_id,
|
||||
location=adjustment.location,
|
||||
quantity=adjustment.quantity,
|
||||
)
|
||||
|
||||
result = inventory_service.adjust_inventory(
|
||||
db=db,
|
||||
vendor_id=adjustment.vendor_id,
|
||||
inventory_data=service_data,
|
||||
)
|
||||
|
||||
sign = "+" if adjustment.quantity >= 0 else ""
|
||||
logger.info(
|
||||
f"Admin {current_admin.email} adjusted inventory for product {adjustment.product_id} "
|
||||
f"at {adjustment.location}: {sign}{adjustment.quantity} units"
|
||||
f"{f' (reason: {adjustment.reason})' if adjustment.reason else ''}"
|
||||
)
|
||||
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@admin_router.put("/{inventory_id}", response_model=InventoryResponse)
|
||||
def update_inventory(
|
||||
inventory_id: int,
|
||||
inventory_update: InventoryUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Update inventory entry fields."""
|
||||
# Get inventory to find vendor_id
|
||||
inventory = inventory_service.get_inventory_by_id_admin(db, inventory_id)
|
||||
|
||||
result = inventory_service.update_inventory(
|
||||
db=db,
|
||||
vendor_id=inventory.vendor_id,
|
||||
inventory_id=inventory_id,
|
||||
inventory_update=inventory_update,
|
||||
)
|
||||
|
||||
logger.info(f"Admin {current_admin.email} updated inventory {inventory_id}")
|
||||
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@admin_router.delete("/{inventory_id}", response_model=InventoryMessageResponse)
|
||||
def delete_inventory(
|
||||
inventory_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Delete inventory entry."""
|
||||
# Get inventory to find vendor_id and log details
|
||||
inventory = inventory_service.get_inventory_by_id_admin(db, inventory_id)
|
||||
vendor_id = inventory.vendor_id
|
||||
product_id = inventory.product_id
|
||||
location = inventory.location
|
||||
|
||||
inventory_service.delete_inventory(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
inventory_id=inventory_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Admin {current_admin.email} deleted inventory {inventory_id} "
|
||||
f"(product {product_id} at {location})"
|
||||
)
|
||||
|
||||
db.commit()
|
||||
return InventoryMessageResponse(message="Inventory deleted successfully")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Import Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class UnmatchedGtin(BaseModel):
|
||||
"""GTIN that couldn't be matched to a product."""
|
||||
|
||||
gtin: str
|
||||
quantity: int
|
||||
product_name: str
|
||||
|
||||
|
||||
class InventoryImportResponse(BaseModel):
|
||||
"""Response from inventory import."""
|
||||
|
||||
success: bool
|
||||
total_rows: int
|
||||
entries_created: int
|
||||
entries_updated: int
|
||||
quantity_imported: int
|
||||
unmatched_gtins: list[UnmatchedGtin]
|
||||
errors: list[str]
|
||||
|
||||
|
||||
@admin_router.post("/import", response_model=InventoryImportResponse)
|
||||
async def import_inventory(
|
||||
file: UploadFile = File(..., description="TSV/CSV file with BIN, EAN, PRODUCT, QUANTITY columns"),
|
||||
vendor_id: int = Form(..., description="Vendor ID"),
|
||||
warehouse: str = Form("strassen", description="Warehouse name"),
|
||||
clear_existing: bool = Form(False, description="Clear existing inventory before import"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Import inventory from a TSV/CSV file.
|
||||
|
||||
File format (TSV recommended):
|
||||
- Required columns: BIN, EAN
|
||||
- Optional columns: PRODUCT (for display), QUANTITY (defaults to 1 per row)
|
||||
|
||||
If QUANTITY column is present, each row represents the quantity specified.
|
||||
If QUANTITY is absent, each row counts as 1 unit (rows with same EAN+BIN are summed).
|
||||
|
||||
Products are matched by GTIN/EAN. Unmatched GTINs are reported in the response.
|
||||
"""
|
||||
# Verify vendor exists
|
||||
inventory_service.verify_vendor_exists(db, vendor_id)
|
||||
|
||||
# Read file content
|
||||
content = await file.read()
|
||||
try:
|
||||
content_str = content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
content_str = content.decode("latin-1")
|
||||
|
||||
# Detect delimiter
|
||||
first_line = content_str.split("\n")[0] if content_str else ""
|
||||
delimiter = "\t" if "\t" in first_line else ","
|
||||
|
||||
# Run import
|
||||
result = inventory_import_service.import_from_text(
|
||||
db=db,
|
||||
content=content_str,
|
||||
vendor_id=vendor_id,
|
||||
warehouse=warehouse,
|
||||
delimiter=delimiter,
|
||||
clear_existing=clear_existing,
|
||||
)
|
||||
|
||||
if result.success:
|
||||
db.commit()
|
||||
logger.info(
|
||||
f"Admin {current_admin.email} imported inventory: "
|
||||
f"{result.entries_created} created, {result.entries_updated} updated, "
|
||||
f"{result.quantity_imported} total units"
|
||||
)
|
||||
else:
|
||||
db.rollback()
|
||||
logger.error(f"Inventory import failed: {result.errors}")
|
||||
|
||||
return InventoryImportResponse(
|
||||
success=result.success,
|
||||
total_rows=result.total_rows,
|
||||
entries_created=result.entries_created,
|
||||
entries_updated=result.entries_updated,
|
||||
quantity_imported=result.quantity_imported,
|
||||
unmatched_gtins=[UnmatchedGtin(**g) for g in result.unmatched_gtins],
|
||||
errors=result.errors,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Transaction History Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.get("/transactions", response_model=AdminInventoryTransactionListResponse)
|
||||
def get_all_transactions(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
vendor_id: int | None = Query(None, description="Filter by vendor"),
|
||||
product_id: int | None = Query(None, description="Filter by product"),
|
||||
transaction_type: str | None = Query(None, description="Filter by type"),
|
||||
order_id: int | None = Query(None, description="Filter by order"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get inventory transaction history across all vendors.
|
||||
|
||||
Returns a paginated list of all stock movements with vendor and product details.
|
||||
"""
|
||||
transactions, total = inventory_transaction_service.get_all_transactions_admin(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
vendor_id=vendor_id,
|
||||
product_id=product_id,
|
||||
transaction_type=transaction_type,
|
||||
order_id=order_id,
|
||||
)
|
||||
|
||||
return AdminInventoryTransactionListResponse(
|
||||
transactions=[AdminInventoryTransactionItem(**tx) for tx in transactions],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/transactions/stats", response_model=AdminTransactionStatsResponse)
|
||||
def get_transaction_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get transaction statistics for the platform."""
|
||||
stats = inventory_transaction_service.get_transaction_stats_admin(db)
|
||||
return AdminTransactionStatsResponse(**stats)
|
||||
260
app/modules/inventory/routes/api/vendor.py
Normal file
260
app/modules/inventory/routes/api/vendor.py
Normal file
@@ -0,0 +1,260 @@
|
||||
# app/modules/inventory/routes/api/vendor.py
|
||||
"""
|
||||
Vendor inventory management endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.services.inventory_service import inventory_service
|
||||
from app.services.inventory_transaction_service import inventory_transaction_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.inventory.schemas import (
|
||||
InventoryAdjust,
|
||||
InventoryCreate,
|
||||
InventoryListResponse,
|
||||
InventoryMessageResponse,
|
||||
InventoryReserve,
|
||||
InventoryResponse,
|
||||
InventoryTransactionListResponse,
|
||||
InventoryTransactionWithProduct,
|
||||
InventoryUpdate,
|
||||
OrderTransactionHistoryResponse,
|
||||
ProductInventorySummary,
|
||||
ProductTransactionHistoryResponse,
|
||||
)
|
||||
|
||||
vendor_router = APIRouter(
|
||||
prefix="/inventory",
|
||||
dependencies=[Depends(require_module_access("inventory"))],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@vendor_router.post("/set", response_model=InventoryResponse)
|
||||
def set_inventory(
|
||||
inventory: InventoryCreate,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Set exact inventory quantity (replaces existing)."""
|
||||
result = inventory_service.set_inventory(
|
||||
db, current_user.token_vendor_id, inventory
|
||||
)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@vendor_router.post("/adjust", response_model=InventoryResponse)
|
||||
def adjust_inventory(
|
||||
adjustment: InventoryAdjust,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Adjust inventory (positive to add, negative to remove)."""
|
||||
result = inventory_service.adjust_inventory(
|
||||
db, current_user.token_vendor_id, adjustment
|
||||
)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@vendor_router.post("/reserve", response_model=InventoryResponse)
|
||||
def reserve_inventory(
|
||||
reservation: InventoryReserve,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Reserve inventory for an order."""
|
||||
result = inventory_service.reserve_inventory(
|
||||
db, current_user.token_vendor_id, reservation
|
||||
)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@vendor_router.post("/release", response_model=InventoryResponse)
|
||||
def release_reservation(
|
||||
reservation: InventoryReserve,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Release reserved inventory (cancel order)."""
|
||||
result = inventory_service.release_reservation(
|
||||
db, current_user.token_vendor_id, reservation
|
||||
)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@vendor_router.post("/fulfill", response_model=InventoryResponse)
|
||||
def fulfill_reservation(
|
||||
reservation: InventoryReserve,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Fulfill reservation (complete order, remove from stock)."""
|
||||
result = inventory_service.fulfill_reservation(
|
||||
db, current_user.token_vendor_id, reservation
|
||||
)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@vendor_router.get("/product/{product_id}", response_model=ProductInventorySummary)
|
||||
def get_product_inventory(
|
||||
product_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get inventory summary for a product."""
|
||||
return inventory_service.get_product_inventory(
|
||||
db, current_user.token_vendor_id, product_id
|
||||
)
|
||||
|
||||
|
||||
@vendor_router.get("", response_model=InventoryListResponse)
|
||||
def get_vendor_inventory(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
location: str | None = Query(None),
|
||||
low_stock: int | None = Query(None, ge=0),
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all inventory for vendor."""
|
||||
inventories = inventory_service.get_vendor_inventory(
|
||||
db, current_user.token_vendor_id, skip, limit, location, low_stock
|
||||
)
|
||||
|
||||
# Get total count
|
||||
total = len(inventories) # You might want a separate count query for large datasets
|
||||
|
||||
return InventoryListResponse(
|
||||
inventories=inventories, total=total, skip=skip, limit=limit
|
||||
)
|
||||
|
||||
|
||||
@vendor_router.put("/{inventory_id}", response_model=InventoryResponse)
|
||||
def update_inventory(
|
||||
inventory_id: int,
|
||||
inventory_update: InventoryUpdate,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update inventory entry."""
|
||||
result = inventory_service.update_inventory(
|
||||
db, current_user.token_vendor_id, inventory_id, inventory_update
|
||||
)
|
||||
db.commit()
|
||||
return result
|
||||
|
||||
|
||||
@vendor_router.delete("/{inventory_id}", response_model=InventoryMessageResponse)
|
||||
def delete_inventory(
|
||||
inventory_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete inventory entry."""
|
||||
inventory_service.delete_inventory(db, current_user.token_vendor_id, inventory_id)
|
||||
db.commit()
|
||||
return InventoryMessageResponse(message="Inventory deleted successfully")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Inventory Transaction History Endpoints
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@vendor_router.get("/transactions", response_model=InventoryTransactionListResponse)
|
||||
def get_inventory_transactions(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
product_id: int | None = Query(None, description="Filter by product"),
|
||||
transaction_type: str | None = Query(None, description="Filter by type"),
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get inventory transaction history for the vendor.
|
||||
|
||||
Returns a paginated list of all stock movements with product details.
|
||||
Use filters to narrow down by product or transaction type.
|
||||
"""
|
||||
transactions, total = inventory_transaction_service.get_vendor_transactions(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
product_id=product_id,
|
||||
transaction_type=transaction_type,
|
||||
)
|
||||
|
||||
return InventoryTransactionListResponse(
|
||||
transactions=[InventoryTransactionWithProduct(**tx) for tx in transactions],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@vendor_router.get(
|
||||
"/transactions/product/{product_id}",
|
||||
response_model=ProductTransactionHistoryResponse,
|
||||
)
|
||||
def get_product_transaction_history(
|
||||
product_id: int,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get transaction history for a specific product.
|
||||
|
||||
Returns recent stock movements with current inventory status.
|
||||
"""
|
||||
result = inventory_transaction_service.get_product_history(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
product_id=product_id,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return ProductTransactionHistoryResponse(**result)
|
||||
|
||||
|
||||
@vendor_router.get(
|
||||
"/transactions/order/{order_id}",
|
||||
response_model=OrderTransactionHistoryResponse,
|
||||
)
|
||||
def get_order_transaction_history(
|
||||
order_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all inventory transactions for a specific order.
|
||||
|
||||
Shows all stock movements (reserve, fulfill, release) related to an order.
|
||||
"""
|
||||
result = inventory_transaction_service.get_order_history(
|
||||
db=db,
|
||||
vendor_id=current_user.token_vendor_id,
|
||||
order_id=order_id,
|
||||
)
|
||||
|
||||
return OrderTransactionHistoryResponse(
|
||||
order_id=result["order_id"],
|
||||
order_number=result["order_number"],
|
||||
transactions=[
|
||||
InventoryTransactionWithProduct(**tx) for tx in result["transactions"]
|
||||
],
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
# app/modules/inventory/routes/vendor.py
|
||||
"""
|
||||
Inventory module vendor routes.
|
||||
|
||||
This module wraps the existing vendor inventory routes and adds
|
||||
module-based access control. Routes are re-exported from the
|
||||
original location with the module access dependency.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.deps import require_module_access
|
||||
|
||||
# Import original router (direct import to avoid circular dependency)
|
||||
from app.api.v1.vendor.inventory import router as original_router
|
||||
|
||||
# Create module-aware router
|
||||
vendor_router = APIRouter(
|
||||
prefix="/inventory",
|
||||
dependencies=[Depends(require_module_access("inventory"))],
|
||||
)
|
||||
|
||||
# Re-export all routes from the original module with module access control
|
||||
for route in original_router.routes:
|
||||
vendor_router.routes.append(route)
|
||||
Reference in New Issue
Block a user