feat(repairs): add repair management foundation

This commit is contained in:
Schubert Ferenc 2026-07-04 11:46:58 +02:00
parent 22e54f212c
commit e9ec207617
37 changed files with 2149 additions and 7 deletions

View file

@ -157,6 +157,12 @@ def action_title(action: str) -> str:
"knowledge.notes.create": "Wissensnotiz erstellt",
"knowledge.notes.update": "Wissensnotiz bearbeitet",
"knowledge.notes.delete": "Wissensnotiz gelöscht",
"repairs.create": "Reparatur erstellt",
"repairs.update": "Reparatur bearbeitet",
"repairs.status_update": "Reparaturstatus geändert",
"repairs.cancel": "Reparatur storniert",
"repairs.intake_processed": "Reparatur-Intake verarbeitet",
"repairs.intake_failed": "Reparatur-Intake fehlgeschlagen",
}
return labels.get(action, action)

View file

@ -0,0 +1,157 @@
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
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},
)
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")