arbret/REFACTOR_PLAN.md
counterweight badb45da59
Extract price logic to PriceService
- Create PriceService with get_recent_prices() and fetch_and_store_price()
- Update routes/audit.py to use PriceService instead of direct queries
- Use PriceHistoryMapper consistently
- Update test to patch services.price.fetch_btc_eur_price
2025-12-25 18:30:26 +01:00

8.3 KiB

Refactoring Plan: Extract Business Logic from Routes

Goal

Remove all business/domain logic from route handlers. Routes should only:

  1. Receive HTTP requests
  2. Call service methods
  3. Map responses using mappers
  4. Return HTTP responses

Current State Analysis

Routes with Business Logic

1. routes/auth.py

Business Logic:

  • register(): Invite validation, user creation, invite marking, role assignment
  • get_default_role(): Database query (should use repository)

Action: Create AuthService with:

  • register_user() - handles entire registration flow
  • login_user() - handles authentication and token creation

2. routes/invites.py

Business Logic:

  • check_invite(): Invite validation logic
  • get_my_invites(): Database query + response building
  • create_invite(): Invite creation with collision retry logic
  • list_all_invites(): Query building, filtering, pagination
  • revoke_invite(): Revocation business logic

Action: Use existing InviteService (already exists but not fully used):

  • Move check_invite() logic to InviteService.check_invite_validity()
  • Move create_invite() logic to InviteService.create_invite()
  • Move revoke_invite() logic to InviteService.revoke_invite()
  • Add InviteService.get_user_invites() for get_my_invites()
  • Add InviteService.list_invites() for list_all_invites()

3. routes/profile.py

Business Logic:

  • get_godfather_email(): Database query (should use repository)
  • get_profile(): Data retrieval and response building
  • update_profile(): Validation and field updates

Action: Create ProfileService with:

  • get_profile() - retrieves profile with godfather email
  • update_profile() - validates and updates profile fields

4. routes/availability.py

Business Logic:

  • get_availability(): Query, grouping by date, transformation
  • set_availability(): Slot overlap validation, time ordering validation, deletion, creation
  • copy_availability(): Source validation, copying logic, atomic transaction handling

Action: Create AvailabilityService with:

  • get_availability_for_range() - gets and groups availability
  • set_availability_for_date() - validates slots and replaces availability
  • copy_availability() - copies availability from one date to others

5. routes/audit.py

Business Logic:

  • get_price_history(): Database query
  • fetch_price_now(): Price fetching, duplicate timestamp handling
  • _to_price_history_response(): Mapping (should use mapper)

Action: Create PriceService with:

  • get_recent_prices() - gets recent price history
  • fetch_and_store_price() - fetches from Bitfinex and stores (handles duplicates)
  • Move _to_price_history_response() to PriceHistoryMapper

6. routes/exchange.py

Business Logic:

  • get_available_slots(): Query, slot expansion logic
  • Enum validation (acceptable - this is input validation at route level)

Action:

  • Move slot expansion logic to ExchangeService or AvailabilityService
  • Keep enum validation in route (it's input validation, not business logic)

Implementation Plan

Phase 1: Create Missing Services

  1. ExchangeService (already exists)
  2. InviteService (already exists, needs expansion)
  3. AuthService (needs creation)
  4. ProfileService (needs creation)
  5. AvailabilityService (needs creation)
  6. PriceService (needs creation)

Phase 2: Expand Existing Services

  1. Expand InviteService:
    • Add get_user_invites()
    • Add list_invites() with pagination
    • Ensure all methods use repositories

Phase 3: Update Routes to Use Services

  1. routes/auth.py → Use AuthService
  2. routes/invites.py → Use InviteService consistently
  3. routes/profile.py → Use ProfileService
  4. routes/availability.py → Use AvailabilityService
  5. routes/audit.py → Use PriceService
  6. routes/exchange.py → Move slot expansion to service

Phase 4: Clean Up

  1. Remove all direct database queries from routes
  2. Remove all business logic from routes
  3. Replace all HTTPException with custom exceptions
  4. Ensure all mappers are used consistently
  5. Remove helper functions from routes (move to services/repositories)

File Structure After Refactoring

backend/
├── routes/
│   ├── auth.py          # Only HTTP handling, calls AuthService
│   ├── invites.py       # Only HTTP handling, calls InviteService
│   ├── profile.py       # Only HTTP handling, calls ProfileService
│   ├── availability.py  # Only HTTP handling, calls AvailabilityService
│   ├── audit.py         # Only HTTP handling, calls PriceService
│   └── exchange.py      # Only HTTP handling, calls ExchangeService
├── services/
│   ├── __init__.py
│   ├── auth.py          # NEW: Registration, login logic
│   ├── invite.py        # EXISTS: Expand with missing methods
│   ├── profile.py       # NEW: Profile CRUD operations
│   ├── availability.py  # NEW: Availability management
│   ├── price.py         # NEW: Price fetching and history
│   └── exchange.py      # EXISTS: Already good, minor additions
├── repositories/
│   └── ... (already good)
└── mappers/
    └── ... (add PriceHistoryMapper)

Detailed Service Specifications

AuthService

class AuthService:
    async def register_user(
        self, 
        email: str, 
        password: str, 
        invite_identifier: str
    ) -> tuple[User, str]:  # Returns (user, token)
        """Register new user with invite validation."""
        
    async def login_user(
        self, 
        email: str, 
        password: str
    ) -> tuple[User, str]:  # Returns (user, token)
        """Authenticate user and create token."""

ProfileService

class ProfileService:
    async def get_profile(self, user: User) -> ProfileResponse:
        """Get user profile with godfather email."""
        
    async def update_profile(
        self, 
        user: User, 
        data: ProfileUpdate
    ) -> ProfileResponse:
        """Validate and update profile fields."""

AvailabilityService

class AvailabilityService:
    async def get_availability_for_range(
        self, 
        from_date: date, 
        to_date: date
    ) -> AvailabilityResponse:
        """Get availability grouped by date."""
        
    async def set_availability_for_date(
        self, 
        target_date: date, 
        slots: list[TimeSlot]
    ) -> AvailabilityDay:
        """Validate and set availability for a date."""
        
    async def copy_availability(
        self, 
        source_date: date, 
        target_dates: list[date]
    ) -> AvailabilityResponse:
        """Copy availability from source to target dates."""

PriceService

class PriceService:
    async def get_recent_prices(self, limit: int = 20) -> list[PriceHistory]:
        """Get recent price history."""
        
    async def fetch_and_store_price(self) -> PriceHistory:
        """Fetch price from Bitfinex and store (handles duplicates)."""

InviteService (Expansion)

class InviteService:
    # Existing methods...
    
    async def get_user_invites(self, user_id: int) -> list[Invite]:
        """Get all invites for a user."""
        
    async def list_invites(
        self,
        page: int,
        per_page: int,
        status_filter: str | None = None,
        godfather_id: int | None = None
    ) -> PaginatedInviteRecords:
        """List invites with pagination and filtering."""

Testing Strategy

  1. Ensure all existing tests pass after each service creation
  2. Add service-level unit tests
  3. Keep route tests focused on HTTP concerns (status codes, response formats)
  4. Move business logic tests to service tests

Migration Order

  1. PriceService (simplest, least dependencies)
  2. AvailabilityService (self-contained)
  3. ProfileService (simple CRUD)
  4. AuthService (more complex, but isolated)
  5. InviteService expansion (already exists, just expand)
  6. ExchangeService (slot expansion logic)

Success Criteria

  • No await db.execute() calls in routes
  • No business validation logic in routes
  • No data transformation logic in routes
  • All routes are thin wrappers around service calls
  • All tests pass
  • Code is more testable and maintainable