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

244 lines
8.3 KiB
Markdown

# 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
```python
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
```python
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
```python
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
```python
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)
```python
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