from datetime import UTC, datetime from sqlalchemy import func, select from sqlalchemy.orm import Session, selectinload from app.models.repair_estimate import RepairEstimate, RepairEstimateEvent, RepairEstimateItem class RepairEstimateRepository: @staticmethod def list_by_repair(db: Session, repair_id: int) -> list[RepairEstimate]: return list( db.scalars( select(RepairEstimate) .options(selectinload(RepairEstimate.items), selectinload(RepairEstimate.events)) .where(RepairEstimate.repair_id == repair_id) .order_by(RepairEstimate.created_at.desc(), RepairEstimate.id.desc()) ) ) @staticmethod def get(db: Session, *, repair_id: int, estimate_id: int) -> RepairEstimate | None: return db.scalar( select(RepairEstimate) .options(selectinload(RepairEstimate.items), selectinload(RepairEstimate.events)) .where(RepairEstimate.repair_id == repair_id) .where(RepairEstimate.id == estimate_id) ) @staticmethod def get_latest_public(db: Session, repair_id: int) -> RepairEstimate | None: return db.scalar( select(RepairEstimate) .options(selectinload(RepairEstimate.items)) .where(RepairEstimate.repair_id == repair_id) .where(RepairEstimate.status.in_(["sent", "approved", "declined", "revoked"])) .order_by(RepairEstimate.sent_at.desc().nullslast(), RepairEstimate.created_at.desc(), RepairEstimate.id.desc()) .limit(1) ) @staticmethod def get_next_number(db: Session, year: int) -> str: prefix = f"KV{year}-" latest = db.scalar( select(RepairEstimate.estimate_number) .where(RepairEstimate.estimate_number.like(f"{prefix}%")) .order_by(RepairEstimate.estimate_number.desc()) .limit(1) ) next_number = 1 if latest: next_number = int(latest.split("-")[-1]) + 1 return f"{prefix}{next_number:06d}" @staticmethod def save(db: Session, estimate: RepairEstimate) -> RepairEstimate: db.add(estimate) db.commit() db.refresh(estimate) return RepairEstimateRepository.get(db, repair_id=estimate.repair_id, estimate_id=estimate.id) or estimate @staticmethod def replace_items(db: Session, estimate: RepairEstimate, items: list[RepairEstimateItem]) -> None: estimate.items.clear() db.flush() for item in items: estimate.items.append(item) @staticmethod def delete(db: Session, estimate: RepairEstimate) -> None: db.delete(estimate) db.commit() @staticmethod def add_event( db: Session, *, estimate_id: int, event_type: str, actor_type: str, actor_user_id: int | None = None, note: str | None = None, commit: bool = True, ) -> RepairEstimateEvent: event = RepairEstimateEvent( estimate_id=estimate_id, event_type=event_type, actor_type=actor_type, actor_user_id=actor_user_id, note=note, ) db.add(event) if commit: db.commit() db.refresh(event) return event @staticmethod def mark_sent(db: Session, estimate: RepairEstimate) -> RepairEstimate: estimate.status = "sent" estimate.sent_at = datetime.now(UTC) db.commit() db.refresh(estimate) return RepairEstimateRepository.get(db, repair_id=estimate.repair_id, estimate_id=estimate.id) or estimate @staticmethod def count_open(db: Session) -> int: return db.scalar(select(func.count(RepairEstimate.id)).where(RepairEstimate.status.in_(["draft", "sent"]))) or 0 @staticmethod def count_waiting(db: Session) -> int: return db.scalar(select(func.count(RepairEstimate.id)).where(RepairEstimate.status == "sent")) or 0 @staticmethod def count_approved_today(db: Session) -> int: today = datetime.now(UTC).date() return db.scalar( select(func.count(RepairEstimate.id)) .where(RepairEstimate.status == "approved") .where(func.date(RepairEstimate.approved_at) == today) ) or 0 @staticmethod def count_declined(db: Session) -> int: return db.scalar(select(func.count(RepairEstimate.id)).where(RepairEstimate.status == "declined")) or 0 @staticmethod def count_revoked_today(db: Session) -> int: today = datetime.now(UTC).date() return db.scalar( select(func.count(RepairEstimate.id)) .join(RepairEstimateEvent, RepairEstimateEvent.estimate_id == RepairEstimate.id) .where(RepairEstimate.status == "revoked") .where(RepairEstimateEvent.event_type == "revoked") .where(func.date(RepairEstimateEvent.created_at) == today) ) or 0