|
| 1 | +import datetime |
| 2 | +import logging |
| 3 | + |
| 4 | +from fastapi import APIRouter, Depends, HTTPException |
| 5 | +from fastapi_sqlalchemy import db |
| 6 | +from pydantic import BaseModel |
| 7 | +from typing import Optional |
| 8 | + |
| 9 | +from models.base import User, Item, ItemLog |
| 10 | +from utils.auth import authenticate |
| 11 | +from utils.gear_lifecycle import replacement_score, get_benchmark |
| 12 | + |
| 13 | +logger = logging.getLogger(__name__) |
| 14 | + |
| 15 | +route = APIRouter(dependencies=[Depends(authenticate)]) |
| 16 | + |
| 17 | +VALID_CONDITIONS = {"new", "good", "fair", "worn", "retired"} |
| 18 | +VALID_STATUSES = {"active", "wishlist", "retired", "sold", "lost"} |
| 19 | +VALID_ACQUISITION_TYPES = {"purchased", "gifted", "traded", "diy"} |
| 20 | +VALID_RETIRED_REASONS = {"worn_out", "upgraded", "lost", "sold", "gifted"} |
| 21 | + |
| 22 | + |
| 23 | +class LifecycleUpdate(BaseModel): |
| 24 | + acquired_date: str = None |
| 25 | + acquisition_type: str = None |
| 26 | + purchase_retailer: str = None |
| 27 | + condition: str = None |
| 28 | + status: str = None |
| 29 | + retired_date: str = None |
| 30 | + retired_reason: str = None |
| 31 | + replaced_by_id: int = None |
| 32 | + |
| 33 | + |
| 34 | +@route.put("/{item_id}/lifecycle") |
| 35 | +def update_lifecycle(item_id: int, payload: LifecycleUpdate, user: User = Depends(authenticate)): |
| 36 | + item = db.session.query(Item).filter_by(id=item_id, user_id=user.id).first() |
| 37 | + if not item: |
| 38 | + raise HTTPException(404, "Item not found.") |
| 39 | + |
| 40 | + if payload.condition and payload.condition not in VALID_CONDITIONS: |
| 41 | + raise HTTPException(400, f"Invalid condition. Must be one of: {', '.join(VALID_CONDITIONS)}") |
| 42 | + |
| 43 | + if payload.status and payload.status not in VALID_STATUSES: |
| 44 | + raise HTTPException(400, f"Invalid status. Must be one of: {', '.join(VALID_STATUSES)}") |
| 45 | + |
| 46 | + if payload.acquisition_type and payload.acquisition_type not in VALID_ACQUISITION_TYPES: |
| 47 | + raise HTTPException(400, f"Invalid acquisition_type. Must be one of: {', '.join(VALID_ACQUISITION_TYPES)}") |
| 48 | + |
| 49 | + if payload.retired_reason and payload.retired_reason not in VALID_RETIRED_REASONS: |
| 50 | + raise HTTPException(400, f"Invalid retired_reason. Must be one of: {', '.join(VALID_RETIRED_REASONS)}") |
| 51 | + |
| 52 | + if payload.replaced_by_id: |
| 53 | + replacement = db.session.query(Item).filter_by( |
| 54 | + id=payload.replaced_by_id, user_id=user.id |
| 55 | + ).first() |
| 56 | + if not replacement: |
| 57 | + raise HTTPException(400, "Replacement item not found.") |
| 58 | + |
| 59 | + old_condition = item.condition |
| 60 | + |
| 61 | + fields = payload.dict(exclude_none=True) |
| 62 | + for key, value in fields.items(): |
| 63 | + setattr(item, key, value) |
| 64 | + |
| 65 | + if payload.condition and payload.condition != old_condition: |
| 66 | + log_entry = ItemLog( |
| 67 | + item_id=item.id, |
| 68 | + user_id=user.id, |
| 69 | + event_type="condition_change", |
| 70 | + event_date=datetime.date.today(), |
| 71 | + old_condition=old_condition, |
| 72 | + new_condition=payload.condition, |
| 73 | + ) |
| 74 | + db.session.add(log_entry) |
| 75 | + |
| 76 | + try: |
| 77 | + db.session.commit() |
| 78 | + db.session.refresh(item) |
| 79 | + except Exception: |
| 80 | + logger.exception("Failed to update item lifecycle") |
| 81 | + raise HTTPException(400, "Unable to update item lifecycle.") |
| 82 | + |
| 83 | + return item |
| 84 | + |
| 85 | + |
| 86 | +VALID_EVENT_TYPES = { |
| 87 | + "acquired", "condition_change", "repair", "maintenance", |
| 88 | + "weight_check", "retired", "sold", "note", |
| 89 | +} |
| 90 | + |
| 91 | + |
| 92 | +class ItemLogCreate(BaseModel): |
| 93 | + event_type: str |
| 94 | + event_date: str |
| 95 | + note: Optional[str] = None |
| 96 | + old_condition: Optional[str] = None |
| 97 | + new_condition: Optional[str] = None |
| 98 | + old_weight: Optional[float] = None |
| 99 | + new_weight: Optional[float] = None |
| 100 | + cost: Optional[float] = None |
| 101 | + |
| 102 | + |
| 103 | +@route.post("/{item_id}/log", status_code=201) |
| 104 | +def create_log(item_id: int, payload: ItemLogCreate, user: User = Depends(authenticate)): |
| 105 | + item = db.session.query(Item).filter_by(id=item_id, user_id=user.id).first() |
| 106 | + if not item: |
| 107 | + raise HTTPException(404, "Item not found.") |
| 108 | + |
| 109 | + if payload.event_type not in VALID_EVENT_TYPES: |
| 110 | + raise HTTPException(400, f"Invalid event_type. Must be one of: {', '.join(VALID_EVENT_TYPES)}") |
| 111 | + |
| 112 | + log_entry = ItemLog( |
| 113 | + item_id=item.id, |
| 114 | + user_id=user.id, |
| 115 | + **payload.dict(), |
| 116 | + ) |
| 117 | + |
| 118 | + try: |
| 119 | + db.session.add(log_entry) |
| 120 | + db.session.commit() |
| 121 | + db.session.refresh(log_entry) |
| 122 | + except Exception: |
| 123 | + logger.exception("Failed to create item log") |
| 124 | + raise HTTPException(400, "Unable to create log entry.") |
| 125 | + |
| 126 | + return log_entry |
| 127 | + |
| 128 | + |
| 129 | +@route.get("/{item_id}/log") |
| 130 | +def fetch_logs(item_id: int, user: User = Depends(authenticate)): |
| 131 | + item = db.session.query(Item).filter_by(id=item_id, user_id=user.id).first() |
| 132 | + if not item: |
| 133 | + raise HTTPException(404, "Item not found.") |
| 134 | + |
| 135 | + logs = db.session.query(ItemLog).filter_by( |
| 136 | + item_id=item.id |
| 137 | + ).order_by(ItemLog.event_date.desc(), ItemLog.created_at.desc()).all() |
| 138 | + |
| 139 | + return logs |
| 140 | + |
| 141 | + |
| 142 | +@route.get("/{item_id}/replacement-score") |
| 143 | +def get_replacement_score(item_id: int, user: User = Depends(authenticate)): |
| 144 | + item = db.session.query(Item).filter_by(id=item_id, user_id=user.id).first() |
| 145 | + if not item: |
| 146 | + raise HTTPException(404, "Item not found.") |
| 147 | + |
| 148 | + category_name = ( |
| 149 | + item.category.category.name |
| 150 | + if item.category and item.category.category |
| 151 | + else "Miscellaneous" |
| 152 | + ) |
| 153 | + |
| 154 | + benchmark = get_benchmark(db.session, user.id, category_name) |
| 155 | + score = replacement_score(item.acquired_date, item.condition, benchmark) |
| 156 | + |
| 157 | + return { |
| 158 | + "item_id": item.id, |
| 159 | + "score": score, |
| 160 | + "category": category_name, |
| 161 | + "benchmark": benchmark, |
| 162 | + "acquired_date": str(item.acquired_date) if item.acquired_date else None, |
| 163 | + "condition": item.condition, |
| 164 | + } |
0 commit comments