201 lines
6.8 KiB
Python
201 lines
6.8 KiB
Python
"""Бизнес-логика работы с миссиями."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime, timezone
|
||
from typing import Any
|
||
|
||
from fastapi import HTTPException, status
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.models.journal import JournalEventType
|
||
from app.models.mission import Mission, MissionFormat, MissionSubmission, SubmissionStatus
|
||
from app.models.user import User, UserArtifact, UserCompetency
|
||
from app.services.journal import log_event
|
||
from app.services.rank import apply_rank_upgrade
|
||
from app.services.storage import delete_submission_document
|
||
|
||
|
||
UNSET: Any = object()
|
||
|
||
|
||
def submit_mission(
|
||
*,
|
||
db: Session,
|
||
user: User,
|
||
mission: Mission,
|
||
comment: str | None,
|
||
proof_url: str | None,
|
||
passport_path: Any = UNSET,
|
||
photo_path: Any = UNSET,
|
||
resume_path: Any = UNSET,
|
||
resume_link: Any = UNSET,
|
||
) -> MissionSubmission:
|
||
"""Создаём или обновляем отправку."""
|
||
|
||
submission = (
|
||
db.query(MissionSubmission)
|
||
.filter(MissionSubmission.user_id == user.id, MissionSubmission.mission_id == mission.id)
|
||
.first()
|
||
)
|
||
if submission and submission.status == SubmissionStatus.APPROVED:
|
||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Миссия уже зачтена")
|
||
|
||
if not submission:
|
||
submission = MissionSubmission(user_id=user.id, mission_id=mission.id)
|
||
|
||
submission.comment = comment
|
||
submission.proof_url = proof_url
|
||
|
||
if passport_path is not UNSET:
|
||
if isinstance(passport_path, str) and submission.passport_path and submission.passport_path != passport_path:
|
||
delete_submission_document(submission.passport_path)
|
||
submission.passport_path = passport_path if isinstance(passport_path, str) else None
|
||
|
||
if photo_path is not UNSET:
|
||
if isinstance(photo_path, str) and submission.photo_path and submission.photo_path != photo_path:
|
||
delete_submission_document(submission.photo_path)
|
||
submission.photo_path = photo_path if isinstance(photo_path, str) else None
|
||
|
||
if resume_path is not UNSET:
|
||
if isinstance(resume_path, str) and submission.resume_path and submission.resume_path != resume_path:
|
||
delete_submission_document(submission.resume_path)
|
||
submission.resume_path = resume_path if isinstance(resume_path, str) else None
|
||
|
||
if resume_link is not UNSET:
|
||
submission.resume_link = resume_link if isinstance(resume_link, str) else None
|
||
|
||
submission.status = SubmissionStatus.PENDING
|
||
|
||
db.add(submission)
|
||
db.commit()
|
||
db.refresh(submission)
|
||
|
||
log_event(
|
||
db,
|
||
user_id=user.id,
|
||
event_type=JournalEventType.MISSION_COMPLETED,
|
||
title=f"Отправка миссии «{mission.title}»",
|
||
description="Отчёт отправлен и ожидает проверки.",
|
||
payload={"mission_id": mission.id},
|
||
)
|
||
|
||
return submission
|
||
|
||
|
||
def _increase_competencies(db: Session, user: User, mission: Mission) -> None:
|
||
"""Повышаем уровни компетенций за миссию."""
|
||
|
||
for reward in mission.competency_rewards:
|
||
user_competency = next(
|
||
(uc for uc in user.competencies if uc.competency_id == reward.competency_id),
|
||
None,
|
||
)
|
||
if not user_competency:
|
||
user_competency = UserCompetency(
|
||
user_id=user.id, competency_id=reward.competency_id, level=0
|
||
)
|
||
db.add(user_competency)
|
||
db.flush()
|
||
user_competency.level += reward.level_delta
|
||
db.add(user_competency)
|
||
|
||
|
||
def approve_submission(db: Session, submission: MissionSubmission) -> MissionSubmission:
|
||
"""Подтверждаем миссию, начисляем награды и проверяем ранг."""
|
||
|
||
if submission.status == SubmissionStatus.APPROVED:
|
||
return submission
|
||
|
||
submission.status = SubmissionStatus.APPROVED
|
||
submission.awarded_xp = submission.mission.xp_reward
|
||
submission.awarded_mana = submission.mission.mana_reward
|
||
|
||
user = submission.user
|
||
user.xp += submission.awarded_xp
|
||
user.mana += submission.awarded_mana
|
||
|
||
_increase_competencies(db, user, submission.mission)
|
||
|
||
if submission.mission.artifact_id:
|
||
already_has = any(
|
||
artifact.artifact_id == submission.mission.artifact_id for artifact in user.artifacts
|
||
)
|
||
if not already_has:
|
||
db.add(UserArtifact(user_id=user.id, artifact_id=submission.mission.artifact_id))
|
||
log_event(
|
||
db,
|
||
user_id=user.id,
|
||
event_type=JournalEventType.MISSION_COMPLETED,
|
||
title=f"Получен артефакт за миссию «{submission.mission.title}»",
|
||
description="Новый артефакт добавлен в коллекцию.",
|
||
payload={"artifact_id": submission.mission.artifact_id},
|
||
)
|
||
|
||
db.add_all([submission, user])
|
||
db.commit()
|
||
db.refresh(submission)
|
||
|
||
log_event(
|
||
db,
|
||
user_id=user.id,
|
||
event_type=JournalEventType.MISSION_COMPLETED,
|
||
title=f"Миссия «{submission.mission.title}» подтверждена",
|
||
description="HR одобрил выполнение миссии.",
|
||
payload={"mission_id": submission.mission_id},
|
||
xp_delta=submission.awarded_xp,
|
||
mana_delta=submission.awarded_mana,
|
||
)
|
||
|
||
apply_rank_upgrade(user, db)
|
||
|
||
return submission
|
||
|
||
|
||
def reject_submission(db: Session, submission: MissionSubmission, comment: str | None = None) -> MissionSubmission:
|
||
"""Отклоняем миссию."""
|
||
|
||
submission.status = SubmissionStatus.REJECTED
|
||
if comment:
|
||
submission.comment = comment
|
||
db.add(submission)
|
||
db.commit()
|
||
db.refresh(submission)
|
||
|
||
log_event(
|
||
db,
|
||
user_id=submission.user_id,
|
||
event_type=JournalEventType.MISSION_COMPLETED,
|
||
title=f"Миссия «{submission.mission.title}» отклонена",
|
||
description=comment or "Проверьте отчёт и отправьте снова.",
|
||
payload={"mission_id": submission.mission_id},
|
||
)
|
||
|
||
return submission
|
||
|
||
|
||
def registration_is_open(
|
||
mission: Mission,
|
||
*,
|
||
participant_count: int,
|
||
now: datetime | None = None,
|
||
) -> bool:
|
||
"""Проверяем, доступна ли запись на офлайн-мероприятие."""
|
||
|
||
if mission.format != MissionFormat.OFFLINE:
|
||
return True
|
||
|
||
current_time = now or datetime.now(timezone.utc)
|
||
|
||
deadline = mission.registration_deadline
|
||
if deadline and deadline.tzinfo is None:
|
||
deadline = deadline.replace(tzinfo=timezone.utc)
|
||
|
||
if deadline and deadline < current_time:
|
||
return False
|
||
|
||
if mission.capacity is not None and participant_count >= mission.capacity:
|
||
return False
|
||
|
||
return mission.is_active
|