refactor: enforce strict architecture rules and add Pydantic response models
- Update architecture rules to be stricter (API-003 now blocks ALL exception raising in endpoints, not just HTTPException) - Update get_current_vendor_api dependency to guarantee token_vendor_id presence - Remove redundant _get_vendor_from_token helpers from all vendor API files - Move vendor access validation to service layer methods - Add Pydantic response models for media, notification, and payment endpoints - Add get_active_vendor_by_code service method for public vendor lookup - Add get_import_job_for_vendor service method with vendor validation - Update validation script to detect exception raising patterns in endpoints 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -453,14 +453,37 @@ current_user: User = Depends(get_current_vendor_from_cookie_or_header)
|
||||
|
||||
**Purpose:** Authenticate vendor users for API endpoints
|
||||
**Accepts:** Authorization header ONLY
|
||||
**Returns:** `User` object with `role="vendor"`
|
||||
**Returns:** `User` object with `role="vendor"` and **guaranteed** attributes:
|
||||
- `current_user.token_vendor_id` - Vendor ID from JWT token
|
||||
- `current_user.token_vendor_code` - Vendor code from JWT token
|
||||
- `current_user.token_vendor_role` - User's role in vendor (owner, manager, etc.)
|
||||
|
||||
**Raises:**
|
||||
- `InvalidTokenException` - No token or invalid token
|
||||
- `InsufficientPermissionsException` - User is not vendor or is admin
|
||||
- `InvalidTokenException` - No token, invalid token, or **missing vendor context in token**
|
||||
- `InsufficientPermissionsException` - User is not vendor, is admin, or lost access to vendor
|
||||
|
||||
**Guarantees:**
|
||||
This dependency **guarantees** that `token_vendor_id` is present. Endpoints should NOT check for its existence:
|
||||
|
||||
```python
|
||||
# ❌ WRONG - Redundant check violates API-003
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("...")
|
||||
|
||||
# ✅ CORRECT - Dependency guarantees this attribute exists
|
||||
vendor_id = current_user.token_vendor_id
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```python
|
||||
current_user: User = Depends(get_current_vendor_api)
|
||||
@router.get("/orders")
|
||||
def get_orders(
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# Safe to use directly - dependency guarantees token_vendor_id
|
||||
orders = order_service.get_vendor_orders(db, current_user.token_vendor_id)
|
||||
return orders
|
||||
```
|
||||
|
||||
#### `get_current_customer_from_cookie_or_header()`
|
||||
|
||||
@@ -195,27 +195,20 @@ def get_current_vendor_api(
|
||||
def get_vendor_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: User = Depends(get_current_vendor_api), # ✅ Only need this
|
||||
current_user: User = Depends(get_current_vendor_api), # ✅ Guarantees token_vendor_id
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all products in vendor catalog.
|
||||
|
||||
Vendor is determined from JWT token (vendor_id claim).
|
||||
The get_current_vendor_api dependency GUARANTEES token_vendor_id is present.
|
||||
"""
|
||||
# Extract vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Use vendor_id from token for business logic
|
||||
# NO validation needed - dependency guarantees token_vendor_id exists
|
||||
products, total = product_service.get_vendor_products(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
vendor_id=current_user.token_vendor_id, # Safe to use directly
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
@@ -223,6 +216,9 @@ def get_vendor_products(
|
||||
return ProductListResponse(products=products, total=total)
|
||||
```
|
||||
|
||||
> **IMPORTANT**: The `get_current_vendor_api()` dependency now **guarantees** that `token_vendor_id` is present.
|
||||
> Endpoints should NOT check for its existence - this would be redundant validation that belongs in the dependency layer.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Step 1: Identify Endpoints Using require_vendor_context()
|
||||
@@ -264,21 +260,14 @@ product = product_service.get_product(db, vendor.id, product_id)
|
||||
|
||||
**After:**
|
||||
```python
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Extract vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Use vendor_id from token
|
||||
product = product_service.get_product(db, vendor_id, product_id)
|
||||
# Use vendor_id from token directly - dependency guarantees it exists
|
||||
product = product_service.get_product(db, current_user.token_vendor_id, product_id)
|
||||
```
|
||||
|
||||
> **NOTE**: Do NOT add validation like `if not hasattr(current_user, "token_vendor_id")`.
|
||||
> The `get_current_vendor_api` dependency guarantees this attribute is present.
|
||||
> Adding such checks violates the architecture rule API-003 (endpoints should not raise exceptions).
|
||||
|
||||
### Step 4: Update Logging References
|
||||
|
||||
**Before:**
|
||||
@@ -325,24 +314,14 @@ def update_product(
|
||||
def update_product(
|
||||
product_id: int,
|
||||
product_data: ProductUpdate,
|
||||
current_user: User = Depends(get_current_vendor_api), # ✅ Only dependency
|
||||
current_user: User = Depends(get_current_vendor_api), # ✅ Guarantees token_vendor_id
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update product in vendor catalog."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Extract vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id # ✅ From token
|
||||
|
||||
# NO validation needed - dependency guarantees token_vendor_id exists
|
||||
product = product_service.update_product(
|
||||
db=db,
|
||||
vendor_id=vendor_id, # ✅ From token
|
||||
vendor_id=current_user.token_vendor_id, # ✅ From token - safe to use directly
|
||||
product_id=product_id,
|
||||
product_update=product_data
|
||||
)
|
||||
@@ -355,6 +334,9 @@ def update_product(
|
||||
return ProductResponse.model_validate(product)
|
||||
```
|
||||
|
||||
> **Architecture Rule API-003**: Endpoints should NOT raise exceptions. The `get_current_vendor_api` dependency
|
||||
> handles all validation and raises `InvalidTokenException` if `token_vendor_id` is missing.
|
||||
|
||||
## Migration Status
|
||||
|
||||
**COMPLETED** - All vendor API endpoints have been migrated to use the token-based vendor context pattern.
|
||||
@@ -498,9 +480,81 @@ def test_vendor_login_and_api_access():
|
||||
# All products should belong to token vendor
|
||||
```
|
||||
|
||||
## Architecture Rules
|
||||
## Architecture Rules and Design Pattern Enforcement
|
||||
|
||||
See `docs/architecture/rules/API-VND-001.md` for the formal architecture rule enforcing this pattern.
|
||||
### The Layered Exception Pattern
|
||||
|
||||
The architecture enforces a strict layered pattern for where exceptions should be raised:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ENDPOINTS (Thin Layer) - app/api/v1/**/*.py │
|
||||
│ │
|
||||
│ ❌ MUST NOT raise exceptions │
|
||||
│ ❌ MUST NOT check hasattr(current_user, 'token_vendor_id') │
|
||||
│ ✅ MUST trust dependencies to handle validation │
|
||||
│ ✅ MUST directly use current_user.token_vendor_id │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────────┐
|
||||
│ DEPENDENCIES (Validation Layer) - app/api/deps.py │
|
||||
│ │
|
||||
│ ✅ MUST raise InvalidTokenException if token_vendor_id missing │
|
||||
│ ✅ MUST validate user still has vendor access │
|
||||
│ ✅ GUARANTEES token_vendor_id, token_vendor_code, token_vendor_role │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────────┐
|
||||
│ SERVICES (Business Logic) - app/services/**/*.py │
|
||||
│ │
|
||||
│ ✅ MUST raise domain exceptions for business rule violations │
|
||||
│ ✅ Examples: VendorNotFoundException, ProductNotFoundException │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────────────────┐
|
||||
│ GLOBAL EXCEPTION HANDLER - app/exceptions/handler.py │
|
||||
│ │
|
||||
│ ✅ Catches all WizamartException subclasses │
|
||||
│ ✅ Converts to appropriate HTTP responses │
|
||||
│ ✅ Provides consistent error formatting │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Enforced by Architecture Validation
|
||||
|
||||
The validation script (`scripts/validate_architecture.py`) enforces these rules:
|
||||
|
||||
**Rule API-003: Endpoints must NOT raise exceptions directly**
|
||||
- Detects `raise HTTPException`, `raise InvalidTokenException`, etc. in endpoint files
|
||||
- Detects redundant validation like `if not hasattr(current_user, 'token_vendor_id')`
|
||||
- Blocks commits via pre-commit hook if violations found
|
||||
|
||||
### Pre-commit Hook
|
||||
|
||||
Architecture validation runs on every commit:
|
||||
|
||||
```yaml
|
||||
# .pre-commit-config.yaml
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-architecture
|
||||
name: Validate Architecture Patterns
|
||||
entry: python scripts/validate_architecture.py
|
||||
language: python
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
```
|
||||
|
||||
To run manually:
|
||||
```bash
|
||||
python scripts/validate_architecture.py # Full validation
|
||||
python scripts/validate_architecture.py -d app/api/v1/vendor/ # Specific directory
|
||||
```
|
||||
|
||||
See `.architecture-rules.yaml` for the complete rule definitions.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
@@ -519,4 +573,4 @@ The vendor-in-token architecture:
|
||||
- ✅ Simplifies endpoint implementation
|
||||
- ✅ Follows architecture best practices
|
||||
|
||||
**Migration Status:** In progress - 9 endpoint files remaining to migrate
|
||||
**Migration Status:** ✅ COMPLETED - All vendor API endpoints migrated and architecture rules enforced
|
||||
|
||||
Reference in New Issue
Block a user