o2o-negosium-original/negodata/backend/scheduler/jobs.py
Mina Choi 7f948075e5 [feat] negodata/backend: 견적 자동마감 스케줄러 + 다음 라운드 재생성(자동/수동)
- APScheduler 크론(KST 5분): 마감일 지난 견적 / 모든 세션 종결 견적 → close_and_decide
- 마감 판정: 단독 최저가 낙찰 확정 / 동가·전원미참여 다음 라운드 재생성 / 거부·한도 마감
- 재생성: 같은 견적번호 체인(round+1, 이름 '(N차)'), 공급사 수로 재협상·재견적, 사유별 한도(미참여1+동가1)
- 수동 재생성 POST /regenerate/{qt_id} (마지막 차수만 허용)
- CloseOutcome enum + 잡 결과별 로그

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:36:22 +09:00

65 lines
3.1 KiB
Python

"""스케줄 잡 로직(what). '언제 도느냐'(scheduler/__init__.py)와 분리된, 잡이 실제로 하는 일.
두 잡 모두 '대상 견적을 골라' → 견적마다 QuotationService.close_and_decide 를 호출한다.
마감 + 결과 판정(낙찰 확정 / 다음 라운드 재생성 / 그냥 마감)은 전부 도메인(close_and_decide)이 책임지고,
여기 잡은 '어떤 견적을 고르냐(대상 선정)'와 '언제 도느냐'만 담당한다.
"""
from collections import Counter
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import quotations
from common.enums import CloseOutcome, DBWRType, ErrorType
from common.logger import LOG
from common.utils.gtime import GTime
from crud.quotation_crud import QuotationCRUD
from services.quotation_service import QuotationService
async def close_expired_quotations() -> int:
"""[잡①] 마감일이 지난 견적을 자동 마감 처리한다. 하루 한 번 실행.
대상: 마감 시각이 이미 지났는데 아직 마감되지 않은(삭제되지도 않은) 견적.
처리: 견적마다 close_and_decide 로 결과 판정(낙찰 확정 / 다음 라운드 재생성 / 그냥 마감).
반환: 처리한 견적 수."""
crud = QuotationCRUD()
service = QuotationService(crud)
now = GTime.UTC()
err_type, qt_ids = await DB_SESSION_MNG.execute_lambda(
quotations.DBType(),
DBWRType.DB_READ.value,
lambda s: crud.list_due_for_close(s, now),
)
if err_type != ErrorType.SUCCESS:
LOG.e_no_callstack(f"[scheduler] close_expired 대상 조회 실패: {err_type.name}")
return 0
results = Counter()
for qt_id in qt_ids:
results[await service.close_and_decide(qt_id)] += 1
if results:
LOG.i(f"[scheduler] close_expired: 낙찰 {results[CloseOutcome.AWARDED]} / 재생성 {results[CloseOutcome.REGENERATED]} / 마감 {results[CloseOutcome.CLOSED]}")
return sum(results.values())
async def close_negotiated_quotations() -> int:
"""[잡②] 모든 세션의 협상이 끝난 견적은 마감일을 기다리지 않고 바로 마감한다(견적 타입 무관).
대상: 아직 마감되지 않았고, 진행중·미시작 세션이 하나도 없는(= 모두 종결된) 견적. 한 시간마다 실행.
처리: 견적마다 close_and_decide 로 결과 판정(낙찰 확정 / 다음 라운드 재생성 / 그냥 마감).
반환: 처리한 견적 수."""
crud = QuotationCRUD()
service = QuotationService(crud)
err_type, qt_ids = await DB_SESSION_MNG.execute_lambda(
quotations.DBType(),
DBWRType.DB_READ.value,
lambda s: crud.list_all_sessions_ended(s),
)
if err_type != ErrorType.SUCCESS:
LOG.e_no_callstack(f"[scheduler] close_negotiated 대상 조회 실패: {err_type.name}")
return 0
results = Counter()
for qt_id in qt_ids:
results[await service.close_and_decide(qt_id)] += 1
if results:
LOG.i(f"[scheduler] close_negotiated: 낙찰 {results[CloseOutcome.AWARDED]} / 재생성 {results[CloseOutcome.REGENERATED]} / 마감 {results[CloseOutcome.CLOSED]}")
return sum(results.values())