from common.database.db_session_manager import DB_SESSION_MNG from common.database.model.models import quotations, sessions from common.enums import DBWRType, ErrorType, QuotationStatus, SessionStatus from common.logger import LOG from common.utils.gtime import GTime from crud.quotation_crud import QuotationCRUD async def close_expired_quotations() -> int: """[잡①] 마감일이 지난 견적을 자동으로 견적마감 처리한다. 하루 한 번 실행. 대상: 마감 시각이 이미 지났는데 아직 마감되지 않은(삭제되지도 않은) 견적. 처리: 그 견적들을 견적마감 상태로 바꾸고, 아직 시작 전인 세션은 미참여로 정리한다. 반환: 마감 처리한 견적 수.""" crud = QuotationCRUD() 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 if not qt_ids: return 0 err_type = await DB_SESSION_MNG.execute_lambda_run( [quotations.DBType()], [ lambda s: crud.bulk_update_quotation_status(s, qt_ids, QuotationStatus.CLOSED.value), lambda s: crud.bulk_update_sessions_status( s, qt_ids, [SessionStatus.CREATED.value, SessionStatus.IN_PROGRESS.value], SessionStatus.NOT_PARTICIPATED.value ), ], ) if err_type != ErrorType.SUCCESS: LOG.e_no_callstack(f"[scheduler] close_expired 마감 실패: {err_type.name}") return 0 LOG.i(f"[scheduler] close_expired: {len(qt_ids)}건 견적마감") return len(qt_ids) async def complete_requote_quotations() -> int: """[잡②] 재견적은 협상완료된 세션이 생기면 나머지를 기다리지 않고 바로 마감한다. 한 시간마다 실행. 대상: 아직 마감되지 않은 재견적 견적 중, 협상완료된 세션이 있는 것. 낙찰: 협상완료된 세션 중 입찰가가 가장 낮은 공급사를 낙찰자로 정한다. 같은 최저가가 둘 이상이면(동가) 낙찰자를 비우고 동가 정보만 남긴다. (현재 재견적은 견적당 세션이 하나라 실제로는 단독 낙찰만 일어나지만, 모델상 1:N이라 일반 규칙을 그대로 둔다.) 처리: 낙찰 정보를 기록하고 견적을 견적마감 상태로 바꾸며, 아직 시작 전인 세션은 미참여로 정리한다. 반환: 마감 처리한 견적 수.""" crud = QuotationCRUD() err_type, qt_ids = await DB_SESSION_MNG.execute_lambda( quotations.DBType(), DBWRType.DB_READ.value, lambda s: crud.list_requote_done(s), ) if err_type != ErrorType.SUCCESS: LOG.e_no_callstack(f"[scheduler] complete_requote 대상 조회 실패: {err_type.name}") return 0 if not qt_ids: return 0 closed = 0 for qt_id in qt_ids: e2, done_rows = await DB_SESSION_MNG.execute_lambda( sessions.DBType(), DBWRType.DB_READ.value, lambda s, q=qt_id: crud.list_done_sessions(s, q), ) if e2 != ErrorType.SUCCESS: LOG.e_no_callstack(f"[scheduler] complete_requote DONE세션 조회 실패 qt_id={qt_id}: {e2.name}") continue # 현재 재견적은 세션이 하나라 사실상 단독 낙찰만 타지만, 모델상 1:N이라 일반 규칙(_pick_winner)을 그대로 쓴다. winner, equal = _pick_winner(done_rows) # 단독 낙찰과 동가는 상호배타(KTC 정본). 플래그를 명시적으로 박는다. data = { "status": QuotationStatus.CLOSED.value, "preferred_sp_yn": winner is not None, "equal_bid_yn": equal is not None, } if winner is not None: data["preferred_sp_id"] = winner["supplier_id"] data["preferred_sp_name"] = (winner["name"] or "")[:20] if equal is not None: data["equal_bid_data"] = equal e3 = await DB_SESSION_MNG.execute_lambda_run( [quotations.DBType()], [ lambda s, d=data, q=qt_id: crud.update_quotation(s, q, d), lambda s, q=qt_id: crud.update_sessions_status( s, q, [SessionStatus.CREATED.value, SessionStatus.IN_PROGRESS.value], SessionStatus.NOT_PARTICIPATED.value ), ], ) if e3 == ErrorType.SUCCESS: closed += 1 else: LOG.e_no_callstack(f"[scheduler] complete_requote 마감 실패 qt_id={qt_id}: {e3.name}") if closed: LOG.i(f"[scheduler] complete_requote: {closed}건 견적마감") return closed def _pick_winner(done_rows): """협상완료된 세션들 중에서 낙찰자를 정한다(KTC 정본 규칙). complete_requote_quotations 전용 헬퍼. 입찰가가 매겨진 세션들 가운데 가장 낮은 가격을 부른 공급사를 낙찰자로 본다. - 최저가를 부른 곳이 한 곳뿐이면: 그 공급사를 낙찰자로 정하고, 동가는 없다. - 최저가가 둘 이상으로 같으면(동가): 낙찰자는 비우고 동가 정보(최저가와 그 공급사들)만 남긴다. - 입찰가가 매겨진 세션이 하나도 없으면: 낙찰자도 동가 정보도 없다. 낙찰자와 동가 정보를 한 쌍으로 돌려주며, 둘은 동시에 채워지지 않는다(단독 낙찰 또는 동가, 둘 중 하나).""" cands = [(sid, int(bp), name) for sid, bp, name in done_rows if bp is not None] if not cands: return None, None min_price = min(c[1] for c in cands) tied = [c for c in cands if c[1] == min_price] if len(tied) > 1: # 동가입찰: 최저가가 여럿 → 낙찰 미지정, 동가만 기록 equal = { "price": min_price, "suppliers": [{"supplier_id": str(sid), "name": name} for sid, _, name in tied], } return None, equal return {"supplier_id": tied[0][0], "name": tied[0][2]}, None