- 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
8.3 KiB
8.3 KiB
Refactoring Plan: Extract Business Logic from Routes
Goal
Remove all business/domain logic from route handlers. Routes should only:
- Receive HTTP requests
- Call service methods
- Map responses using mappers
- Return HTTP responses
Current State Analysis
Routes with Business Logic
1. routes/auth.py
Business Logic:
register(): Invite validation, user creation, invite marking, role assignmentget_default_role(): Database query (should use repository)
Action: Create AuthService with:
register_user()- handles entire registration flowlogin_user()- handles authentication and token creation
2. routes/invites.py
Business Logic:
check_invite(): Invite validation logicget_my_invites(): Database query + response buildingcreate_invite(): Invite creation with collision retry logiclist_all_invites(): Query building, filtering, paginationrevoke_invite(): Revocation business logic
Action: Use existing InviteService (already exists but not fully used):
- Move
check_invite()logic toInviteService.check_invite_validity() - Move
create_invite()logic toInviteService.create_invite() - Move
revoke_invite()logic toInviteService.revoke_invite() - Add
InviteService.get_user_invites()forget_my_invites() - Add
InviteService.list_invites()forlist_all_invites()
3. routes/profile.py
Business Logic:
get_godfather_email(): Database query (should use repository)get_profile(): Data retrieval and response buildingupdate_profile(): Validation and field updates
Action: Create ProfileService with:
get_profile()- retrieves profile with godfather emailupdate_profile()- validates and updates profile fields
4. routes/availability.py
Business Logic:
get_availability(): Query, grouping by date, transformationset_availability(): Slot overlap validation, time ordering validation, deletion, creationcopy_availability(): Source validation, copying logic, atomic transaction handling
Action: Create AvailabilityService with:
get_availability_for_range()- gets and groups availabilityset_availability_for_date()- validates slots and replaces availabilitycopy_availability()- copies availability from one date to others
5. routes/audit.py
Business Logic:
get_price_history(): Database queryfetch_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 historyfetch_and_store_price()- fetches from Bitfinex and stores (handles duplicates)- Move
_to_price_history_response()toPriceHistoryMapper
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
ExchangeServiceorAvailabilityService - Keep enum validation in route (it's input validation, not business logic)
Implementation Plan
Phase 1: Create Missing Services
- ✅
ExchangeService(already exists) - ✅
InviteService(already exists, needs expansion) - ❌
AuthService(needs creation) - ❌
ProfileService(needs creation) - ❌
AvailabilityService(needs creation) - ❌
PriceService(needs creation)
Phase 2: Expand Existing Services
- Expand
InviteService:- Add
get_user_invites() - Add
list_invites()with pagination - Ensure all methods use repositories
- Add
Phase 3: Update Routes to Use Services
routes/auth.py→ UseAuthServiceroutes/invites.py→ UseInviteServiceconsistentlyroutes/profile.py→ UseProfileServiceroutes/availability.py→ UseAvailabilityServiceroutes/audit.py→ UsePriceServiceroutes/exchange.py→ Move slot expansion to service
Phase 4: Clean Up
- Remove all direct database queries from routes
- Remove all business logic from routes
- Replace all
HTTPExceptionwith custom exceptions - Ensure all mappers are used consistently
- 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
- Ensure all existing tests pass after each service creation
- Add service-level unit tests
- Keep route tests focused on HTTP concerns (status codes, response formats)
- Move business logic tests to service tests
Migration Order
- PriceService (simplest, least dependencies)
- AvailabilityService (self-contained)
- ProfileService (simple CRUD)
- AuthService (more complex, but isolated)
- InviteService expansion (already exists, just expand)
- 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