refactor: fix all architecture validator findings (202 → 0)
Eliminate all 103 errors and 96 warnings from the architecture validator: Phase 1 - Validator rules & YAML: - Add NAM-001/NAM-002 exceptions for module-scoped router/service files - Fix API-004 to detect # public comments on decorator lines - Add module-specific exception bases to EXC-004 valid_bases - Exclude storefront files from AUTH-004 store context check - Add SVC-006 exceptions for loyalty service atomic commits - Fix _get_rule() to search naming_rules and auth_rules categories - Use plain # CODE comments instead of # noqa: CODE for custom rules Phase 2 - Billing module (5 route files): - Move _resolve_store_to_merchant to subscription_service - Move tier/feature queries to feature_service, admin_subscription_service - Extract 22 inline Pydantic schemas to billing/schemas/billing.py - Replace all HTTPException with domain exceptions Phase 3 - Loyalty module (4 routes + points_service): - Add 7 domain exceptions (Apple auth, enrollment, device registration) - Add service methods to card_service, program_service, apple_wallet_service - Move all db.query() from routes to service layer - Fix SVC-001: replace HTTPException in points_service with domain exception Phase 4 - Remaining modules: - tenancy: move store stats queries to admin_service - cms: move platform resolution to content_page_service, add NoPlatformSubscriptionException - messaging: move user/customer lookups to messaging_service - Add ConfigDict(from_attributes=True) to ContentPageResponse Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,16 +15,12 @@ Cards can be used at any store within the same merchant.
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_store_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.loyalty.exceptions import (
|
||||
LoyaltyCardNotFoundException,
|
||||
LoyaltyException,
|
||||
)
|
||||
from app.modules.loyalty.schemas import (
|
||||
CardEnrollRequest,
|
||||
CardListResponse,
|
||||
@@ -63,7 +59,7 @@ from app.modules.loyalty.services import (
|
||||
program_service,
|
||||
stamp_service,
|
||||
)
|
||||
from app.modules.tenancy.models import Store, User
|
||||
from app.modules.tenancy.models import User # API-007
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -83,10 +79,7 @@ def get_client_info(request: Request) -> tuple[str | None, str | None]:
|
||||
|
||||
def get_store_merchant_id(db: Session, store_id: int) -> int:
|
||||
"""Get the merchant ID for a store."""
|
||||
store = db.query(Store).filter(Store.id == store_id).first()
|
||||
if not store:
|
||||
raise HTTPException(status_code=404, detail="Store not found")
|
||||
return store.merchant_id
|
||||
return program_service.get_store_merchant_id(db, store_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -102,9 +95,7 @@ def get_program(
|
||||
"""Get the merchant's loyalty program."""
|
||||
store_id = current_user.token_store_id
|
||||
|
||||
program = program_service.get_program_by_store(db, store_id)
|
||||
if not program:
|
||||
raise HTTPException(status_code=404, detail="No loyalty program configured")
|
||||
program = program_service.require_program_by_store(db, store_id)
|
||||
|
||||
response = ProgramResponse.model_validate(program)
|
||||
response.is_stamps_enabled = program.is_stamps_enabled
|
||||
@@ -124,10 +115,7 @@ def create_program(
|
||||
store_id = current_user.token_store_id
|
||||
merchant_id = get_store_merchant_id(db, store_id)
|
||||
|
||||
try:
|
||||
program = program_service.create_program(db, merchant_id, data)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
program = program_service.create_program(db, merchant_id, data)
|
||||
|
||||
response = ProgramResponse.model_validate(program)
|
||||
response.is_stamps_enabled = program.is_stamps_enabled
|
||||
@@ -146,10 +134,7 @@ def update_program(
|
||||
"""Update the merchant's loyalty program."""
|
||||
store_id = current_user.token_store_id
|
||||
|
||||
program = program_service.get_program_by_store(db, store_id)
|
||||
if not program:
|
||||
raise HTTPException(status_code=404, detail="No loyalty program configured")
|
||||
|
||||
program = program_service.require_program_by_store(db, store_id)
|
||||
program = program_service.update_program(db, program.id, data)
|
||||
|
||||
response = ProgramResponse.model_validate(program)
|
||||
@@ -168,9 +153,7 @@ def get_stats(
|
||||
"""Get loyalty program statistics."""
|
||||
store_id = current_user.token_store_id
|
||||
|
||||
program = program_service.get_program_by_store(db, store_id)
|
||||
if not program:
|
||||
raise HTTPException(status_code=404, detail="No loyalty program configured")
|
||||
program = program_service.require_program_by_store(db, store_id)
|
||||
|
||||
stats = program_service.get_program_stats(db, program.id)
|
||||
return ProgramStatsResponse(**stats)
|
||||
@@ -186,8 +169,6 @@ def get_merchant_stats(
|
||||
merchant_id = get_store_merchant_id(db, store_id)
|
||||
|
||||
stats = program_service.get_merchant_stats(db, merchant_id)
|
||||
if "error" in stats:
|
||||
raise HTTPException(status_code=404, detail=stats["error"])
|
||||
|
||||
return MerchantStatsResponse(**stats)
|
||||
|
||||
@@ -205,9 +186,7 @@ def list_pins(
|
||||
"""List staff PINs for this store location."""
|
||||
store_id = current_user.token_store_id
|
||||
|
||||
program = program_service.get_program_by_store(db, store_id)
|
||||
if not program:
|
||||
raise HTTPException(status_code=404, detail="No loyalty program configured")
|
||||
program = program_service.require_program_by_store(db, store_id)
|
||||
|
||||
# List PINs for this store only
|
||||
pins = pin_service.list_pins(db, program.id, store_id=store_id)
|
||||
@@ -227,9 +206,7 @@ def create_pin(
|
||||
"""Create a new staff PIN for this store location."""
|
||||
store_id = current_user.token_store_id
|
||||
|
||||
program = program_service.get_program_by_store(db, store_id)
|
||||
if not program:
|
||||
raise HTTPException(status_code=404, detail="No loyalty program configured")
|
||||
program = program_service.require_program_by_store(db, store_id)
|
||||
|
||||
pin = pin_service.create_pin(db, program.id, store_id, data)
|
||||
return PinResponse.model_validate(pin)
|
||||
@@ -292,9 +269,7 @@ def list_cards(
|
||||
store_id = current_user.token_store_id
|
||||
merchant_id = get_store_merchant_id(db, store_id)
|
||||
|
||||
program = program_service.get_program_by_store(db, store_id)
|
||||
if not program:
|
||||
raise HTTPException(status_code=404, detail="No loyalty program configured")
|
||||
program = program_service.require_program_by_store(db, store_id)
|
||||
|
||||
# Filter by enrolled_at_store_id if requested
|
||||
filter_store_id = store_id if enrolled_here else None
|
||||
@@ -352,17 +327,15 @@ def lookup_card(
|
||||
"""
|
||||
store_id = current_user.token_store_id
|
||||
|
||||
try:
|
||||
# Uses lookup_card_for_store which validates merchant membership
|
||||
card = card_service.lookup_card_for_store(
|
||||
db,
|
||||
store_id,
|
||||
card_id=card_id,
|
||||
qr_code=qr_code,
|
||||
card_number=card_number,
|
||||
)
|
||||
except LoyaltyCardNotFoundException:
|
||||
raise HTTPException(status_code=404, detail="Card not found")
|
||||
# Uses lookup_card_for_store which validates merchant membership
|
||||
# Raises LoyaltyCardNotFoundException if not found
|
||||
card = card_service.lookup_card_for_store(
|
||||
db,
|
||||
store_id,
|
||||
card_id=card_id,
|
||||
qr_code=qr_code,
|
||||
card_number=card_number,
|
||||
)
|
||||
|
||||
program = card.program
|
||||
|
||||
@@ -420,13 +393,14 @@ def enroll_customer(
|
||||
"""
|
||||
store_id = current_user.token_store_id
|
||||
|
||||
if not data.customer_id:
|
||||
raise HTTPException(status_code=400, detail="customer_id is required")
|
||||
customer_id = card_service.resolve_customer_id(
|
||||
db,
|
||||
customer_id=data.customer_id,
|
||||
email=data.email,
|
||||
store_id=store_id,
|
||||
)
|
||||
|
||||
try:
|
||||
card = card_service.enroll_customer_for_store(db, data.customer_id, store_id)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
card = card_service.enroll_customer_for_store(db, customer_id, store_id)
|
||||
|
||||
program = card.program
|
||||
|
||||
@@ -461,11 +435,8 @@ def get_card_transactions(
|
||||
"""Get transaction history for a card."""
|
||||
store_id = current_user.token_store_id
|
||||
|
||||
# Verify card belongs to this merchant
|
||||
try:
|
||||
card_service.lookup_card_for_store(db, store_id, card_id=card_id)
|
||||
except LoyaltyCardNotFoundException:
|
||||
raise HTTPException(status_code=404, detail="Card not found")
|
||||
# Verify card belongs to this merchant (raises LoyaltyCardNotFoundException if not found)
|
||||
card_service.lookup_card_for_store(db, store_id, card_id=card_id)
|
||||
|
||||
transactions, total = card_service.get_card_transactions(
|
||||
db, card_id, skip=skip, limit=limit
|
||||
@@ -493,20 +464,17 @@ def add_stamp(
|
||||
store_id = current_user.token_store_id
|
||||
ip, user_agent = get_client_info(request)
|
||||
|
||||
try:
|
||||
result = stamp_service.add_stamp(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
result = stamp_service.add_stamp(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
|
||||
return StampResponse(**result)
|
||||
|
||||
@@ -522,20 +490,17 @@ def redeem_stamps(
|
||||
store_id = current_user.token_store_id
|
||||
ip, user_agent = get_client_info(request)
|
||||
|
||||
try:
|
||||
result = stamp_service.redeem_stamps(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
result = stamp_service.redeem_stamps(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
|
||||
return StampRedeemResponse(**result)
|
||||
|
||||
@@ -551,22 +516,19 @@ def void_stamps(
|
||||
store_id = current_user.token_store_id
|
||||
ip, user_agent = get_client_info(request)
|
||||
|
||||
try:
|
||||
result = stamp_service.void_stamps(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
stamps_to_void=data.stamps_to_void,
|
||||
original_transaction_id=data.original_transaction_id,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
result = stamp_service.void_stamps(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
stamps_to_void=data.stamps_to_void,
|
||||
original_transaction_id=data.original_transaction_id,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
|
||||
return StampVoidResponse(**result)
|
||||
|
||||
@@ -587,22 +549,19 @@ def earn_points(
|
||||
store_id = current_user.token_store_id
|
||||
ip, user_agent = get_client_info(request)
|
||||
|
||||
try:
|
||||
result = points_service.earn_points(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
purchase_amount_cents=data.purchase_amount_cents,
|
||||
order_reference=data.order_reference,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
result = points_service.earn_points(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
purchase_amount_cents=data.purchase_amount_cents,
|
||||
order_reference=data.order_reference,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
|
||||
return PointsEarnResponse(**result)
|
||||
|
||||
@@ -618,21 +577,18 @@ def redeem_points(
|
||||
store_id = current_user.token_store_id
|
||||
ip, user_agent = get_client_info(request)
|
||||
|
||||
try:
|
||||
result = points_service.redeem_points(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
reward_id=data.reward_id,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
result = points_service.redeem_points(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
reward_id=data.reward_id,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
|
||||
return PointsRedeemResponse(**result)
|
||||
|
||||
@@ -648,23 +604,20 @@ def void_points(
|
||||
store_id = current_user.token_store_id
|
||||
ip, user_agent = get_client_info(request)
|
||||
|
||||
try:
|
||||
result = points_service.void_points(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
points_to_void=data.points_to_void,
|
||||
original_transaction_id=data.original_transaction_id,
|
||||
order_reference=data.order_reference,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
result = points_service.void_points(
|
||||
db,
|
||||
store_id=store_id,
|
||||
card_id=data.card_id,
|
||||
qr_code=data.qr_code,
|
||||
card_number=data.card_number,
|
||||
points_to_void=data.points_to_void,
|
||||
original_transaction_id=data.original_transaction_id,
|
||||
order_reference=data.order_reference,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
notes=data.notes,
|
||||
)
|
||||
|
||||
return PointsVoidResponse(**result)
|
||||
|
||||
@@ -681,18 +634,15 @@ def adjust_points(
|
||||
store_id = current_user.token_store_id
|
||||
ip, user_agent = get_client_info(request)
|
||||
|
||||
try:
|
||||
result = points_service.adjust_points(
|
||||
db,
|
||||
card_id=card_id,
|
||||
points_delta=data.points_delta,
|
||||
store_id=store_id,
|
||||
reason=data.reason,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
except LoyaltyException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
result = points_service.adjust_points(
|
||||
db,
|
||||
card_id=card_id,
|
||||
points_delta=data.points_delta,
|
||||
store_id=store_id,
|
||||
reason=data.reason,
|
||||
staff_pin=data.staff_pin,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
|
||||
return PointsAdjustResponse(**result)
|
||||
|
||||
Reference in New Issue
Block a user