# Architecture Rules - Service Layer Rules # Rules for app/services/**/*.py and app/modules/*/services/**/*.py files service_layer_rules: - id: "SVC-001" name: "Service must NOT raise HTTPException" severity: "error" description: | Services are business logic layer - they should NOT know about HTTP. Raise domain-specific exceptions instead (ValueError, custom exceptions). pattern: file_pattern: - "app/services/**/*.py" - "app/modules/*/services/**/*.py" anti_patterns: - "raise HTTPException" - "from fastapi import HTTPException" - id: "SVC-002" name: "Service must use proper exception handling" severity: "error" description: | Services should raise meaningful domain exceptions, not generic Exception. Create custom exception classes for business rule violations. pattern: file_pattern: - "app/services/**/*.py" - "app/modules/*/services/**/*.py" discouraged_patterns: - "raise Exception\\(" - id: "SVC-003" name: "Service methods must accept db session as parameter" severity: "error" description: | Service methods should receive database session as a parameter for testability and transaction control. Never create session inside service. pattern: file_pattern: - "app/services/**/*.py" - "app/modules/*/services/**/*.py" required_in_method_signature: - "db: Session" anti_patterns: - "SessionLocal()" - "get_db()" - id: "SVC-004" name: "Service must use Pydantic models for input validation" severity: "warning" description: | Service methods should accept Pydantic models for complex inputs to ensure type safety and validation. pattern: file_pattern: - "app/services/**/*.py" - "app/modules/*/services/**/*.py" encouraged_patterns: - "BaseModel" - id: "SVC-005" name: "Service must scope queries to vendor_id in multi-tenant contexts" severity: "error" description: | All database queries must be scoped to vendor_id to prevent cross-tenant data access. pattern: file_pattern: - "app/services/**/*.py" - "app/modules/*/services/**/*.py" check: "vendor_scoping" - id: "SVC-006" name: "Service must NOT call db.commit()" severity: "warning" description: | Services should NOT commit transactions. Transaction control belongs at the API endpoint level where one request = one transaction. This allows: - Composing multiple service calls in a single transaction - Clean rollback on any failure - Easier testing of services in isolation The endpoint should call db.commit() after all service operations succeed. pattern: file_pattern: - "app/services/**/*.py" - "app/modules/*/services/**/*.py" anti_patterns: - "db.commit()" exceptions: - "log_service.py" - "card_service.py" - "wallet_service.py" - "program_service.py" - "points_service.py" - "apple_wallet_service.py" - "pin_service.py" - "stamp_service.py" - "google_wallet_service.py" - "theme_presets.py" - id: "SVC-007" name: "Service return types must match API response schemas" severity: "error" description: | When a service method's return value will be used as an API response, the returned dict keys MUST match the corresponding Pydantic schema fields. This prevents the common bug where: - Service returns {"total_imports": 5, "completed_imports": 3} - Schema expects {"total": 5, "completed": 3} - Frontend receives wrong/empty values RECOMMENDED PATTERNS: 1. Return Pydantic model directly from service: def get_stats(self, db: Session) -> StatsResponse: return StatsResponse(total=count, completed=done) 2. Return dict with schema-matching keys: def get_stats(self, db: Session) -> dict: return {"total": count, "completed": done} # Matches StatsResponse 3. Document the expected schema in service docstring: def get_stats(self, db: Session) -> dict: """ Returns dict compatible with StatsResponse schema. Keys: total, pending, completed, failed """ TESTING: Write tests that validate service output against schema: result = service.get_stats(db) StatsResponse(**result) # Raises if keys don't match pattern: file_pattern: - "app/services/**/*.py" - "app/modules/*/services/**/*.py" check: "schema_compatibility"