From d2063f6dad173cff9baaee55256b29ee475b6a0a Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Thu, 4 Dec 2025 23:38:12 +0100 Subject: [PATCH] fix: add Pydantic models for customer/inventory endpoints and align JS rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Pydantic response models for vendor customer endpoints - Add InventoryMessageResponse for delete endpoint - Align JS rule IDs between YAML and validation script (JS-001=logger, JS-002=apiClient) - Add exception for init-*.js files in console logging check 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .architecture-rules.yaml | 2 + app/api/v1/vendor/customers.py | 58 ++++++++++++---------- app/api/v1/vendor/inventory.py | 5 +- models/schema/customer.py | 71 +++++++++++++++++++++++++++ models/schema/inventory.py | 6 +++ scripts/validate_architecture.py | 84 +++++++++++++++++--------------- 6 files changed, 159 insertions(+), 67 deletions(-) diff --git a/.architecture-rules.yaml b/.architecture-rules.yaml index e43d8ed0..ba41ab2d 100644 --- a/.architecture-rules.yaml +++ b/.architecture-rules.yaml @@ -432,6 +432,8 @@ javascript_rules: exceptions: - "// eslint-disable" - "console.log('✅" # Bootstrap messages allowed + auto_exclude_files: + - "init-*.js" # Init files run before logger is available - id: "JS-002" name: "Use lowercase apiClient for API calls" diff --git a/app/api/v1/vendor/customers.py b/app/api/v1/vendor/customers.py index bf546b1e..4de51dfd 100644 --- a/app/api/v1/vendor/customers.py +++ b/app/api/v1/vendor/customers.py @@ -15,12 +15,20 @@ from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.services.vendor_service import vendor_service from models.database.user import User +from models.schema.customer import ( + CustomerDetailResponse, + CustomerMessageResponse, + CustomerOrdersResponse, + CustomerStatisticsResponse, + CustomerUpdate, + VendorCustomerListResponse, +) router = APIRouter(prefix="/customers") logger = logging.getLogger(__name__) -@router.get("") +@router.get("", response_model=VendorCustomerListResponse) def get_vendor_customers( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), @@ -39,16 +47,16 @@ def get_vendor_customers( - Return paginated results """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841 - return { - "customers": [], - "total": 0, - "skip": skip, - "limit": limit, - "message": "Customer management coming in Slice 4", - } + return VendorCustomerListResponse( + customers=[], + total=0, + skip=skip, + limit=limit, + message="Customer management coming in Slice 4", + ) -@router.get("/{customer_id}") +@router.get("/{customer_id}", response_model=CustomerDetailResponse) def get_customer_details( customer_id: int, current_user: User = Depends(get_current_vendor_api), @@ -64,10 +72,10 @@ def get_customer_details( - Include total spent, etc. """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841 - return {"message": "Customer details coming in Slice 4"} + return CustomerDetailResponse(message="Customer details coming in Slice 4") -@router.get("/{customer_id}/orders") +@router.get("/{customer_id}/orders", response_model=CustomerOrdersResponse) def get_customer_orders( customer_id: int, current_user: User = Depends(get_current_vendor_api), @@ -82,13 +90,13 @@ def get_customer_orders( - Return order details """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841 - return {"orders": [], "message": "Customer orders coming in Slice 5"} + return CustomerOrdersResponse(orders=[], message="Customer orders coming in Slice 5") -@router.put("/{customer_id}") +@router.put("/{customer_id}", response_model=CustomerMessageResponse) def update_customer( customer_id: int, - customer_data: dict, + customer_data: CustomerUpdate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): @@ -101,10 +109,10 @@ def update_customer( - Update customer preferences """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841 - return {"message": "Customer update coming in Slice 4"} + return CustomerMessageResponse(message="Customer update coming in Slice 4") -@router.put("/{customer_id}/status") +@router.put("/{customer_id}/status", response_model=CustomerMessageResponse) def toggle_customer_status( customer_id: int, current_user: User = Depends(get_current_vendor_api), @@ -119,10 +127,10 @@ def toggle_customer_status( - Log the change """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841 - return {"message": "Customer status toggle coming in Slice 4"} + return CustomerMessageResponse(message="Customer status toggle coming in Slice 4") -@router.get("/{customer_id}/stats") +@router.get("/{customer_id}/stats", response_model=CustomerStatisticsResponse) def get_customer_statistics( customer_id: int, current_user: User = Depends(get_current_vendor_api), @@ -138,10 +146,10 @@ def get_customer_statistics( - Last order date """ vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id) # noqa: F841 - return { - "total_orders": 0, - "total_spent": 0.0, - "average_order_value": 0.0, - "last_order_date": None, - "message": "Customer statistics coming in Slice 4", - } + return CustomerStatisticsResponse( + total_orders=0, + total_spent=0.0, + average_order_value=0.0, + last_order_date=None, + message="Customer statistics coming in Slice 4", + ) diff --git a/app/api/v1/vendor/inventory.py b/app/api/v1/vendor/inventory.py index 66d82068..72947297 100644 --- a/app/api/v1/vendor/inventory.py +++ b/app/api/v1/vendor/inventory.py @@ -18,6 +18,7 @@ from models.schema.inventory import ( InventoryAdjust, InventoryCreate, InventoryListResponse, + InventoryMessageResponse, InventoryReserve, InventoryResponse, InventoryUpdate, @@ -123,7 +124,7 @@ def update_inventory( ) -@router.delete("/inventory/{inventory_id}") +@router.delete("/inventory/{inventory_id}", response_model=InventoryMessageResponse) def delete_inventory( inventory_id: int, current_user: User = Depends(get_current_vendor_api), @@ -131,4 +132,4 @@ def delete_inventory( ): """Delete inventory entry.""" inventory_service.delete_inventory(db, current_user.token_vendor_id, inventory_id) - return {"message": "Inventory deleted successfully"} + return InventoryMessageResponse(message="Inventory deleted successfully") diff --git a/models/schema/customer.py b/models/schema/customer.py index ad788d84..58cc891b 100644 --- a/models/schema/customer.py +++ b/models/schema/customer.py @@ -172,3 +172,74 @@ class CustomerPreferencesUpdate(BaseModel): language: str | None = Field(None, max_length=10) currency: str | None = Field(None, max_length=3) notification_preferences: dict[str, bool] | None = None + + +# ============================================================================ +# Vendor Customer Management Response Schemas +# ============================================================================ + + +class CustomerMessageResponse(BaseModel): + """Simple message response for customer operations.""" + + message: str + + +class VendorCustomerListResponse(BaseModel): + """Schema for vendor customer list with skip/limit pagination.""" + + customers: list[CustomerResponse] = [] + total: int = 0 + skip: int = 0 + limit: int = 100 + message: str | None = None + + +class CustomerDetailResponse(BaseModel): + """Detailed customer response for vendor management.""" + + id: int | None = None + vendor_id: int | None = None + email: str | None = None + first_name: str | None = None + last_name: str | None = None + phone: str | None = None + customer_number: str | None = None + marketing_consent: bool | None = None + last_order_date: datetime | None = None + total_orders: int | None = None + total_spent: Decimal | None = None + is_active: bool | None = None + created_at: datetime | None = None + updated_at: datetime | None = None + message: str | None = None + + model_config = {"from_attributes": True} + + +class CustomerOrderInfo(BaseModel): + """Basic order info for customer order history.""" + + id: int + order_number: str + status: str + total: Decimal + created_at: datetime + + +class CustomerOrdersResponse(BaseModel): + """Response for customer order history.""" + + orders: list[CustomerOrderInfo] = [] + total: int = 0 + message: str | None = None + + +class CustomerStatisticsResponse(BaseModel): + """Response for customer statistics.""" + + total_orders: int = 0 + total_spent: float = 0.0 + average_order_value: float = 0.0 + last_order_date: datetime | None = None + message: str | None = None diff --git a/models/schema/inventory.py b/models/schema/inventory.py index 96f432be..5bf3245b 100644 --- a/models/schema/inventory.py +++ b/models/schema/inventory.py @@ -82,3 +82,9 @@ class InventoryListResponse(BaseModel): total: int skip: int limit: int + + +class InventoryMessageResponse(BaseModel): + """Simple message response for inventory operations.""" + + message: str diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py index a7b8bcd4..b20398a3 100755 --- a/scripts/validate_architecture.py +++ b/scripts/validate_architecture.py @@ -334,14 +334,32 @@ class ArchitectureValidator: """Validate a single JavaScript file""" print("🟨 Validating JavaScript...") - # JS-001: Check for window.apiClient + # JS-001: Check for console usage (must use centralized logger) + # Skip init-*.js files - they run before logger is available + if not file_path.name.startswith("init-"): + for i, line in enumerate(lines, 1): + if re.search(r"console\.(log|warn|error)", line): + if "//" in line or "✅" in line or "eslint-disable" in line: + continue + self._add_violation( + rule_id="JS-001", + rule_name="Use centralized logger", + severity=Severity.WARNING, + file_path=file_path, + line_number=i, + message="Use centralized logger instead of console", + context=line.strip()[:80], + suggestion="Use window.LogConfig.createLogger('moduleName')", + ) + + # JS-002: Check for window.apiClient (must use lowercase apiClient) for i, line in enumerate(lines, 1): if "window.apiClient" in line: before_occurrence = line[: line.find("window.apiClient")] if "//" not in before_occurrence: self._add_violation( - rule_id="JS-001", - rule_name="Use apiClient directly", + rule_id="JS-002", + rule_name="Use lowercase apiClient", severity=Severity.WARNING, file_path=file_path, line_number=i, @@ -350,22 +368,6 @@ class ArchitectureValidator: suggestion="Replace window.apiClient with apiClient", ) - # JS-002: Check for console usage - for i, line in enumerate(lines, 1): - if re.search(r"console\.(log|warn|error)", line): - if "//" in line or "✅" in line or "eslint-disable" in line: - continue - self._add_violation( - rule_id="JS-002", - rule_name="Use centralized logger", - severity=Severity.WARNING, - file_path=file_path, - line_number=i, - message="Use centralized logger instead of console", - context=line.strip()[:80], - suggestion="Use window.LogConfig.createLogger('moduleName')", - ) - def _validate_html_file(self, file_path: Path, content: str, lines: list[str]): """Validate a single HTML template file""" print("📄 Validating template...") @@ -870,15 +872,35 @@ class ArchitectureValidator: content = file_path.read_text() lines = content.split("\n") - # JS-001: Check for window.apiClient + # JS-001: Check for console usage (must use centralized logger) + # Skip init-*.js files - they run before logger is available + if not file_path.name.startswith("init-"): + for i, line in enumerate(lines, 1): + if re.search(r"console\.(log|warn|error)", line): + # Skip if it's a comment or bootstrap message + if "//" in line or "✅" in line or "eslint-disable" in line: + continue + + self._add_violation( + rule_id="JS-001", + rule_name="Use centralized logger", + severity=Severity.WARNING, + file_path=file_path, + line_number=i, + message="Use centralized logger instead of console", + context=line.strip()[:80], + suggestion="Use window.LogConfig.createLogger('moduleName')", + ) + + # JS-002: Check for window.apiClient (must use lowercase apiClient) for i, line in enumerate(lines, 1): if "window.apiClient" in line: # Check if it's not in a comment before_occurrence = line[: line.find("window.apiClient")] if "//" not in before_occurrence: self._add_violation( - rule_id="JS-001", - rule_name="Use apiClient directly", + rule_id="JS-002", + rule_name="Use lowercase apiClient", severity=Severity.WARNING, file_path=file_path, line_number=i, @@ -887,24 +909,6 @@ class ArchitectureValidator: suggestion="Replace window.apiClient with apiClient", ) - # JS-002: Check for console usage - for i, line in enumerate(lines, 1): - if re.search(r"console\.(log|warn|error)", line): - # Skip if it's a comment or bootstrap message - if "//" in line or "✅" in line or "eslint-disable" in line: - continue - - self._add_violation( - rule_id="JS-002", - rule_name="Use centralized logger", - severity=Severity.WARNING, - file_path=file_path, - line_number=i, - message="Use centralized logger instead of console", - context=line.strip()[:80], - suggestion="Use window.LogConfig.createLogger('moduleName')", - ) - def _validate_templates(self, target_path: Path): """Validate template patterns""" print("📄 Validating templates...")