Olympus/backend/hermes/app/services/repair_service.py
2026-07-04 22:58:47 +02:00

171 lines
6.9 KiB
Python

from datetime import UTC, datetime
from fastapi import HTTPException, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from starlette.requests import Request
from app.models.repair import Repair
from app.models.user import User
from app.repositories.customer_repository import CustomerRepository
from app.repositories.repair_repository import RepairRepository
from app.schemas.repair import RepairCreate, RepairIntakePayload, RepairStatusUpdate, RepairUpdate
from app.services.audit_service import sanitize, write_audit_log
from app.services.repair_notification_service import RepairNotificationService
def repair_label(repair: Repair) -> str:
return f"{repair.repair_number} · {repair.customer_name}"
class RepairService:
@staticmethod
def create(db: Session, payload: RepairCreate, *, actor: User | None, request: Request | None = None) -> Repair:
RepairService._validate_customer(db, payload.customer_id)
repair_number = RepairRepository.get_next_number(db, datetime.now(UTC).year)
try:
repair = RepairRepository.create(db, payload, repair_number=repair_number)
except IntegrityError:
db.rollback()
repair_number = RepairRepository.get_next_number(db, datetime.now(UTC).year)
repair = RepairRepository.create(db, payload, repair_number=repair_number)
write_audit_log(
db,
action="repairs.create",
entity_type="repairs",
entity_id=repair.id,
entity_label=repair_label(repair),
actor=actor,
request=request,
after_data=repair,
)
return repair
@staticmethod
def update(db: Session, repair: Repair, payload: RepairUpdate, *, actor: User, request: Request) -> Repair:
RepairService._validate_customer(db, payload.customer_id)
before_data = sanitize(repair)
updated = RepairRepository.update(db, repair, payload)
write_audit_log(
db,
action="repairs.update",
entity_type="repairs",
entity_id=updated.id,
entity_label=repair_label(updated),
actor=actor,
request=request,
before_data=before_data,
after_data=updated,
)
return updated
@staticmethod
def update_status(db: Session, repair: Repair, payload: RepairStatusUpdate, *, actor: User, request: Request) -> Repair:
before_data = sanitize(repair)
updated = RepairRepository.update_status(db, repair, payload, actor_user_id=actor.id)
write_audit_log(
db,
action="repairs.status_update",
entity_type="repairs",
entity_id=updated.id,
entity_label=repair_label(updated),
actor=actor,
request=request,
before_data=before_data,
after_data=updated,
metadata={"old_status": before_data.get("status") if isinstance(before_data, dict) else None, "new_status": updated.status},
)
try:
RepairNotificationService.send_status_mail(db, updated, actor=actor, request=request)
except Exception:
write_audit_log(
db,
action="repairs.status_mail.failed",
entity_type="repairs",
entity_id=updated.id,
entity_label=repair_label(updated),
actor=actor,
request=request,
metadata={"reason": "unexpected_notification_error"},
)
return updated
@staticmethod
def cancel(db: Session, repair: Repair, *, actor: User, request: Request) -> Repair:
before_data = sanitize(repair)
updated = RepairRepository.cancel(db, repair, actor_user_id=actor.id)
write_audit_log(
db,
action="repairs.cancel",
entity_type="repairs",
entity_id=updated.id,
entity_label=repair_label(updated),
actor=actor,
request=request,
before_data=before_data,
after_data=updated,
)
return updated
@staticmethod
def create_from_intake(db: Session, payload: RepairIntakePayload, *, request: Request | None = None) -> Repair:
event = RepairRepository.create_intake_event(
db,
external_source="website",
external_reference=payload.source_reference,
payload=payload.model_dump(mode="json"),
status="received",
)
try:
repair_payload = RepairCreate(
customer_id=None,
contact_id=None,
status="new",
priority="normal",
source="website",
source_reference=payload.source_reference,
customer_name=payload.name,
customer_email=payload.email,
customer_phone=payload.phone,
device_manufacturer=payload.manufacturer,
device_model=payload.model,
device_serial_number=payload.serial_number,
device_type=payload.device_type,
accessories=payload.accessories,
fault_description=payload.fault_description,
previous_work=payload.previous_work,
device_opened=payload.device_opened,
intake_notes="Website Intake",
)
repair = RepairService.create(db, repair_payload, actor=None, request=request)
RepairRepository.update_intake_event(db, event, status="processed", repair_id=repair.id)
write_audit_log(
db,
action="repairs.intake_processed",
entity_type="repairs",
entity_id=repair.id,
entity_label=repair_label(repair),
request=request,
metadata={"external_source": "website", "external_reference": payload.source_reference},
)
return repair
except Exception as exc:
RepairRepository.update_intake_event(db, event, status="failed", error_message="Intake konnte nicht verarbeitet werden")
write_audit_log(
db,
action="repairs.intake_failed",
entity_type="repairs",
request=request,
metadata={"external_source": "website", "external_reference": payload.source_reference},
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Reparaturannahme konnte nicht verarbeitet werden",
) from exc
@staticmethod
def _validate_customer(db: Session, customer_id: int | None) -> None:
if customer_id is None:
return
if CustomerRepository.get_by_id(db, customer_id) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kunde nicht gefunden")