This commit is contained in:
@@ -40,6 +40,7 @@ from app.schemas.service_center import (
|
||||
ServiceCenterVerificationRead,
|
||||
ServiceEmployeeInvite,
|
||||
ServiceEmployeeRead,
|
||||
ServiceEmployeeUpdate,
|
||||
ServiceInboxCreate,
|
||||
ServiceInboxRead,
|
||||
ServiceVisitCreate,
|
||||
@@ -55,6 +56,41 @@ from app.services.vehicle_identity import mask_license_plate, mask_vin
|
||||
router = APIRouter(prefix="/service-centers", tags=["service-centers"])
|
||||
|
||||
APPROVED_SERVICE_STATUSES = {"approved", "verified"}
|
||||
SERVICE_EMPLOYEE_ROLES = {"owner", "manager", "receptionist", "mechanic"}
|
||||
SERVICE_EMPLOYEE_STATUSES = {"active", "invited", "inactive", "revoked", "expired"}
|
||||
|
||||
|
||||
def validate_employee_role(role: str) -> str:
|
||||
role = role.strip().lower()
|
||||
if role not in SERVICE_EMPLOYEE_ROLES:
|
||||
raise HTTPException(status_code=400, detail="Unsupported service employee role")
|
||||
return role
|
||||
|
||||
|
||||
def validate_employee_status(status_value: str) -> str:
|
||||
status_value = status_value.strip().lower()
|
||||
if status_value not in SERVICE_EMPLOYEE_STATUSES:
|
||||
raise HTTPException(status_code=400, detail="Unsupported service employee status")
|
||||
return status_value
|
||||
|
||||
|
||||
async def attach_employee_user_fields(session: AsyncSession, employees: list[ServiceEmployee]) -> list[ServiceEmployee]:
|
||||
if not employees:
|
||||
return employees
|
||||
user_ids = [employee.user_id for employee in employees]
|
||||
users = {
|
||||
user.id: user
|
||||
for user in (await session.execute(select(User).where(User.id.in_(user_ids)))).scalars()
|
||||
}
|
||||
for employee in employees:
|
||||
user = users.get(employee.user_id)
|
||||
if user is None:
|
||||
continue
|
||||
employee.telegram_id = user.telegram_id
|
||||
employee.username = user.username
|
||||
employee.first_name = user.first_name
|
||||
employee.last_name = user.last_name
|
||||
return employees
|
||||
|
||||
@router.post("", response_model=ServiceCenterRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_service_center(
|
||||
@@ -265,7 +301,9 @@ async def invite_employee(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceEmployee:
|
||||
await check_rate_limit(scope="employee_invite", limit=10, window_seconds=3600, request=request, user=current_user, session=session)
|
||||
await ensure_service_center_approved(session, service_center_id)
|
||||
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager"})
|
||||
role = validate_employee_role(payload.role)
|
||||
user = await get_or_create_telegram_user(session, telegram_id=payload.telegram_id)
|
||||
result = await session.execute(
|
||||
select(ServiceEmployee).where(
|
||||
@@ -278,7 +316,7 @@ async def invite_employee(
|
||||
employee = ServiceEmployee(
|
||||
service_center_id=service_center_id,
|
||||
user_id=user.id,
|
||||
role=payload.role,
|
||||
role=role,
|
||||
permissions=payload.permissions,
|
||||
status="invited",
|
||||
invite_token=secrets.token_urlsafe(32),
|
||||
@@ -286,7 +324,9 @@ async def invite_employee(
|
||||
)
|
||||
session.add(employee)
|
||||
else:
|
||||
employee.role = payload.role
|
||||
if employee.role == "owner":
|
||||
raise HTTPException(status_code=409, detail="Owner role cannot be replaced by invite")
|
||||
employee.role = role
|
||||
employee.permissions = payload.permissions
|
||||
employee.status = "invited"
|
||||
employee.invite_token = secrets.token_urlsafe(32)
|
||||
@@ -296,9 +336,27 @@ async def invite_employee(
|
||||
await log_audit(session, actor=current_user, action="service_employee.invite", target_type="service_center", target_id=service_center_id, metadata={"telegram_id": payload.telegram_id})
|
||||
await session.commit()
|
||||
await session.refresh(employee)
|
||||
await attach_employee_user_fields(session, [employee])
|
||||
return employee
|
||||
|
||||
|
||||
@router.get("/{service_center_id}/employees", response_model=list[ServiceEmployeeRead])
|
||||
async def list_service_employees(
|
||||
service_center_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> list[ServiceEmployee]:
|
||||
await ensure_service_center_approved(session, service_center_id)
|
||||
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager"})
|
||||
result = await session.execute(
|
||||
select(ServiceEmployee)
|
||||
.where(ServiceEmployee.service_center_id == service_center_id)
|
||||
.order_by(ServiceEmployee.role.asc(), ServiceEmployee.created_at.asc())
|
||||
)
|
||||
employees = list(result.scalars())
|
||||
return await attach_employee_user_fields(session, employees)
|
||||
|
||||
|
||||
@router.post("/employees/invites/{invite_token}/accept", response_model=ServiceEmployeeRead)
|
||||
async def accept_employee_invite(
|
||||
invite_token: str,
|
||||
@@ -340,6 +398,47 @@ async def accept_employee_invite(
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(employee)
|
||||
await attach_employee_user_fields(session, [employee])
|
||||
return employee
|
||||
|
||||
|
||||
@router.patch("/employees/{employee_id}", response_model=ServiceEmployeeRead)
|
||||
async def update_service_employee(
|
||||
employee_id: int,
|
||||
payload: ServiceEmployeeUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceEmployee:
|
||||
employee = await session.get(ServiceEmployee, employee_id)
|
||||
if employee is None:
|
||||
raise HTTPException(status_code=404, detail="Employee not found")
|
||||
await ensure_service_center_approved(session, employee.service_center_id)
|
||||
await ensure_service_employee(session, employee.service_center_id, current_user, {"owner", "manager"})
|
||||
if employee.role == "owner" and payload.role and payload.role != "owner":
|
||||
raise HTTPException(status_code=409, detail="Owner role cannot be changed")
|
||||
if employee.role == "owner" and payload.status and payload.status != "active":
|
||||
raise HTTPException(status_code=409, detail="Owner cannot be deactivated")
|
||||
data = payload.model_dump(exclude_unset=True)
|
||||
if "role" in data and data["role"] is not None:
|
||||
employee.role = validate_employee_role(data["role"])
|
||||
if "status" in data and data["status"] is not None:
|
||||
employee.status = validate_employee_status(data["status"])
|
||||
if employee.status != "invited":
|
||||
employee.invite_token = None
|
||||
employee.invite_revoked_at = datetime.now(UTC) if employee.status == "revoked" else employee.invite_revoked_at
|
||||
if "permissions" in data:
|
||||
employee.permissions = data["permissions"]
|
||||
await log_audit(
|
||||
session,
|
||||
actor=current_user,
|
||||
action="service_employee.update",
|
||||
target_type="service_employee",
|
||||
target_id=employee.id,
|
||||
metadata=data,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(employee)
|
||||
await attach_employee_user_fields(session, [employee])
|
||||
return employee
|
||||
|
||||
|
||||
@@ -367,6 +466,7 @@ async def revoke_employee_invite(
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(employee)
|
||||
await attach_employee_user_fields(session, [employee])
|
||||
return employee
|
||||
|
||||
|
||||
|
||||
@@ -83,10 +83,20 @@ class ServiceEmployeeInvite(BaseModel):
|
||||
expires_in_hours: int = Field(default=72, ge=0, le=720)
|
||||
|
||||
|
||||
class ServiceEmployeeUpdate(BaseModel):
|
||||
role: str | None = None
|
||||
permissions: dict | None = None
|
||||
status: str | None = None
|
||||
|
||||
|
||||
class ServiceEmployeeRead(BaseModel):
|
||||
id: int
|
||||
service_center_id: int
|
||||
user_id: int
|
||||
telegram_id: int | None = None
|
||||
username: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
role: str
|
||||
permissions: dict | None = None
|
||||
status: str
|
||||
|
||||
Reference in New Issue
Block a user