o2o-negosium-original/negodata/backend/router/v1/quotation/protocol.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

165 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import uuid
from datetime import datetime
from typing import Any, Optional
from pydantic import ConfigDict
from common.enums import CardType, ChatSender, DeliveryType, QuotationStatus, QuotationType, SessionStatus
from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol
class QuotationProtocol(WebPacketProtocol):
pass
class Req_CreateQuotation(QuotationProtocol):
qt_setting_id: uuid.UUID
version_id: Optional[uuid.UUID] = None # 미지정 시 기본 전략 버전(card.versions 시드)으로 채움
name: str = ""
type: int = 0
status: int = 0
start_time: Optional[datetime] = None # 미지정 시 생성 시각(UTC)
end_time: datetime
round: int = 1
manager_name: Optional[str] = None
manager_email: Optional[str] = None
manager_contact_number: Optional[str] = None
memo: Optional[str] = None
item_ids: list[uuid.UUID] = [] # 협상 대상 상품. item×supplier 조합마다 세션 1개 생성
supplier_ids: list[uuid.UUID] = [] # 협상 초청 공급사
card_ids: list[uuid.UUID] = [] # 선택 협상카드. 버전을 만들어 묶고 quotation.version_id 로 연결
class Req_RegenerateQuotation(QuotationProtocol):
supplier_ids: list[uuid.UUID] = [] # 다음 라운드에 부를 공급사(프론트 선택). 상품·기간·번호는 원 견적에서 이어받음
class QuotationData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
qt_id: uuid.UUID
user_id: uuid.UUID
qt_setting_id: uuid.UUID
version_id: uuid.UUID
name: str
number: str
type: QuotationType
round: int = 1
status: QuotationStatus
start_time: datetime
end_time: datetime
manager_name: Optional[str] = None
manager_email: Optional[str] = None
manager_contact_number: Optional[str] = None
memo: Optional[str] = None
iteration: int = 0
preferred_sp_yn: Optional[bool] = None
preferred_sp_id: Optional[uuid.UUID] = None
preferred_sp_name: Optional[str] = None
equal_bid_yn: Optional[bool] = None
equal_bid_data: Optional[Any] = None
participation_count: int = 0 # 견적별 참여 협력사 수(세션 distinct supplier). 목록 집계로 채움.
item_id: Optional[uuid.UUID] = None # 대표 상품 id(세션의 첫 item). 목록 조인으로 채움.
item_name: Optional[str] = None # 대표 상품명. 목록 조인으로 채움.
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Res_Quotation(Res_WebPacketProtocol):
quotation: Optional[QuotationData] = None
class Res_QuotationList(Res_PageProtocol):
quotations: list[QuotationData] = []
class Res_DeleteQuotation(Res_WebPacketProtocol):
pass
class SessionData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
session_id: uuid.UUID
qt_id: uuid.UUID
supplier_id: uuid.UUID
item_id: uuid.UUID
qt_number: str
qt_round: int
qt_type: QuotationType
target_price: int
status: SessionStatus
bid_price: Optional[int] = None
bid_at: Optional[datetime] = None
end_time: datetime
reject_reason: Optional[str] = None
reject_price: Optional[int] = None
reject_delivery_type: Optional[DeliveryType] = None
url: str = "" # 세션 chat 실행 URL(공급사 협상 프론트). DB 미저장 — session_id 로 구성
class Res_CreateQuotation(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None # 생성된 견적 id(프론트가 상세 오버레이 오픈에 사용)
session_count: int = 0 # 함께 생성된 협상 세션 수(토스트 표시용). 세션 풀바디는 별도 GET 으로 조회
class Res_QuotationStatus(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None
job_status: int = 0
message: str = ""
class Res_QuotationSessions(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None
sessions: list[SessionData] = []
total: int = 0
class ChatMessageData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
chat_id: uuid.UUID
session_id: uuid.UUID
card_id: Optional[uuid.UUID] = None
index: int
sender: ChatSender
target_price: int
card_used_yn: Optional[bool] = None
indicator_value: Optional[float] = None
card_type: Optional[CardType] = None
script: Optional[str] = None
step: Optional[str] = None
class Res_SessionChat(Res_WebPacketProtocol):
session_id: Optional[uuid.UUID] = None
messages: list[ChatMessageData] = []
class Res_QuotationResult(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None
winner_supplier_id: Optional[uuid.UUID] = None
winner_supplier_name: Optional[str] = None
is_equal_bid: Optional[bool] = None
equal_bid_data: Optional[Any] = None
result_count: int = 0
class QuotationCardData(WebPacketProtocol):
session_card_id: uuid.UUID
qt_id: Optional[uuid.UUID] = None
nego_card_id: Optional[uuid.UUID] = None
wild_card_id: Optional[uuid.UUID] = None
type: Optional[CardType] = None
number: Optional[str] = None
name: Optional[str] = None
script: Optional[str] = None # 협상 멘트(평문)
edit_script: Optional[Any] = None # 협상 멘트(Slate 서식본)
condition: Optional[str] = None # 와일드카드 전용: 사용 조건(트리거)
memo: Optional[str] = None # 와일드카드 전용: 메모
class Res_QuotationCards(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None
cards: list[QuotationCardData] = []