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:
2025-12-04 23:26:03 +01:00
parent cbfbbb4654
commit 81bfc49f77
25 changed files with 1225 additions and 530 deletions

View File

@@ -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()`

View File

@@ -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