285 lines
12 KiB
Python
285 lines
12 KiB
Python
"""견적 조회(목록·상세·상태·결과·세션·채팅·카드)·삭제와 공용 단건조회(_fetch)."""
|
|
import uuid
|
|
|
|
from common.authz import is_owner_or_admin
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.database.model.models import quotations, sessions, chats
|
|
from common.enums import DBWRType, ErrorType
|
|
from common.models.gmodel import PageParams
|
|
from router.v1.quotation.protocol import (
|
|
ChatMessageData,
|
|
QuotationCardData,
|
|
QuotationData,
|
|
SessionData,
|
|
Res_DeleteQuotation,
|
|
Res_Quotation,
|
|
Res_QuotationCards,
|
|
Res_QuotationList,
|
|
Res_QuotationResult,
|
|
Res_QuotationSessions,
|
|
Res_QuotationStatus,
|
|
Res_SessionChat,
|
|
)
|
|
|
|
|
|
class QueriesMixin:
|
|
async def _fetch(self, qt_id: uuid.UUID, company_id=None):
|
|
"""견적 단건 조회. (ErrorType, quotation|None) 반환.
|
|
company_id 가 주어지면 회사 스코프(작성자 회사) 가드 — 남의 회사 견적은 NOT_FOUND. 내부/스케줄러 호출은 None."""
|
|
err_type, quotation = await DB_SESSION_MNG.execute_lambda(
|
|
quotations.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.get_by_id(s, qt_id, company_id),
|
|
)
|
|
if err_type != ErrorType.SUCCESS or quotation is None:
|
|
return ErrorType.QUOTATION_NOT_FOUND, None
|
|
return ErrorType.SUCCESS, quotation
|
|
|
|
async def list_quotations(self, company_id, owner, search, status, type_, start_from, start_to, pg: PageParams) -> Res_QuotationList:
|
|
"""견적 목록. 회사(company_id) 스코프로 제한하고, owner(user_id) 가 주어지면 '내 견적만'으로 더 좁힌다."""
|
|
res = Res_QuotationList(page=pg.page, size=pg.size)
|
|
|
|
company_uuid = uuid.UUID(company_id)
|
|
owner_uuid = uuid.UUID(owner) if owner else None
|
|
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
|
|
quotations.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.search(s, company_uuid, owner_uuid, search, status, type_, start_from, start_to, pg.skip, pg.size),
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
# 참여 협력사 수(세션 distinct supplier)·대표 상품(세션 item)·작성자명(user→users.name)을
|
|
# 이 페이지 견적들에 대해 각각 한 방으로 모아 합친다(메인 쿼리 비건드림).
|
|
qt_ids = [r.qt_id for r in rows]
|
|
counts = {}
|
|
item_map = {}
|
|
name_map = {}
|
|
if qt_ids:
|
|
cnt_err, got = await DB_SESSION_MNG.execute_lambda(
|
|
quotations.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.session_counts(s, qt_ids),
|
|
)
|
|
if cnt_err == ErrorType.SUCCESS:
|
|
counts = got
|
|
im_err, got_im = await DB_SESSION_MNG.execute_lambda(
|
|
quotations.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.item_map(s, qt_ids),
|
|
)
|
|
if im_err == ErrorType.SUCCESS:
|
|
item_map = got_im
|
|
user_ids = list({r.user_id for r in rows})
|
|
nm_err, got_nm = await DB_SESSION_MNG.execute_lambda(
|
|
quotations.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.user_name_map(s, user_ids),
|
|
)
|
|
if nm_err == ErrorType.SUCCESS:
|
|
name_map = got_nm
|
|
for r in rows:
|
|
r.participation_count = counts.get(r.qt_id, 0)
|
|
item = item_map.get(r.qt_id)
|
|
if item:
|
|
r.item_id, r.item_name = item
|
|
r.creator_name = name_map.get(r.user_id)
|
|
|
|
res.quotations = [QuotationData.model_validate(r) for r in rows]
|
|
res.total = total
|
|
return res
|
|
|
|
async def get_quotation(self, qt_id: str, company_id=None) -> Res_Quotation:
|
|
res = Res_Quotation()
|
|
err_type, quotation = await self._fetch(uuid.UUID(qt_id), company_id)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
res.quotation = QuotationData.model_validate(quotation)
|
|
return res
|
|
|
|
async def delete_quotation(self, qt_id: str, company_id=None, user_id=None, role=None) -> Res_DeleteQuotation:
|
|
res = Res_DeleteQuotation()
|
|
qt_uuid = uuid.UUID(qt_id)
|
|
|
|
err_type, original = await self._fetch(qt_uuid, company_id)
|
|
if err_type != ErrorType.SUCCESS or original is None:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
# 소유자 게이팅 — 본인 견적 또는 최고관리자만 삭제(user_id 미지정=내부 호출은 스킵).
|
|
if user_id is not None and not is_owner_or_admin(original.user_id, user_id, role):
|
|
res.result.SetResult(ErrorType.ACCOUNT_FORBIDDEN)
|
|
res.msg = "본인이 생성한 견적만 삭제할 수 있습니다."
|
|
return res
|
|
|
|
err_type = await DB_SESSION_MNG.execute_lambda_run(
|
|
[quotations.DBType()],
|
|
[lambda s: self.quotation_crud.soft_delete(s, qt_uuid)],
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
async def get_status(self, qt_id: str, company_id=None) -> Res_QuotationStatus:
|
|
res = Res_QuotationStatus()
|
|
err_type, quotation = await self._fetch(uuid.UUID(qt_id), company_id)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
res.qt_id = quotation.qt_id
|
|
res.job_status = quotation.status
|
|
res.message = "ok"
|
|
return res
|
|
|
|
async def get_result(self, qt_id: str, company_id=None) -> Res_QuotationResult:
|
|
res = Res_QuotationResult()
|
|
err_type, quotation = await self._fetch(uuid.UUID(qt_id), company_id)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
# 낙찰 결과는 quotations 컬럼에서 직접 노출. results 테이블 미존재로 result_count 는 0.
|
|
res.qt_id = quotation.qt_id
|
|
res.winner_supplier_id = quotation.preferred_sp_id
|
|
res.winner_supplier_name = quotation.preferred_sp_name
|
|
res.is_equal_bid = quotation.equal_bid_yn
|
|
res.equal_bid_data = quotation.equal_bid_data
|
|
res.result_count = 0
|
|
return res
|
|
|
|
async def list_sessions(self, qt_id: str, company_id=None) -> Res_QuotationSessions:
|
|
res = Res_QuotationSessions()
|
|
qt_uuid = uuid.UUID(qt_id)
|
|
err_type, quotation = await self._fetch(qt_uuid, company_id)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
err_type, rows = await DB_SESSION_MNG.execute_lambda(
|
|
sessions.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.list_sessions(s, qt_uuid),
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
res.qt_id = quotation.qt_id
|
|
# sessions.quotation_id → SessionData.qt_id 로 명시 매핑(컬럼명 불일치).
|
|
res.sessions = [
|
|
SessionData(
|
|
session_id=r.session_id,
|
|
qt_id=r.quotation_id,
|
|
supplier_id=r.supplier_id,
|
|
item_id=r.item_id,
|
|
qt_number=r.qt_number,
|
|
qt_round=r.qt_round,
|
|
qt_type=r.qt_type,
|
|
target_price=r.target_price,
|
|
anchoring_price=r.anchoring_price,
|
|
status=r.status,
|
|
bid_price=r.bid_price,
|
|
bid_at=r.bid_at,
|
|
end_time=r.end_time,
|
|
reject_reason=r.reject_reason,
|
|
reject_price=r.reject_price,
|
|
reject_delivery_type=r.reject_delivery_type,
|
|
email_sent_at=r.email_sent_at,
|
|
custom=r.custom,
|
|
url=self._session_chat_url(r.session_id),
|
|
)
|
|
for r in rows
|
|
]
|
|
res.total = len(res.sessions)
|
|
return res
|
|
|
|
async def list_chats(self, session_id: str, company_id=None) -> Res_SessionChat:
|
|
res = Res_SessionChat()
|
|
sess_uuid = uuid.UUID(session_id)
|
|
res.session_id = sess_uuid
|
|
|
|
# 회사 가드: chats 는 session 키라 세션→견적→회사로 확인한다(남의 회사 세션이면 NOT_FOUND).
|
|
if company_id is not None:
|
|
g_err, got = await DB_SESSION_MNG.execute_lambda(
|
|
sessions.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.get_session_with_supplier(s, sess_uuid),
|
|
)
|
|
if g_err != ErrorType.SUCCESS or got is None:
|
|
res.result.SetResult(g_err if g_err != ErrorType.SUCCESS else ErrorType.QUOTATION_NOT_FOUND)
|
|
return res
|
|
guard_err, _ = await self._fetch(got[0].quotation_id, company_id)
|
|
if guard_err != ErrorType.SUCCESS:
|
|
res.result.SetResult(guard_err)
|
|
return res
|
|
|
|
err_type, rows = await DB_SESSION_MNG.execute_lambda(
|
|
chats.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.list_chats(s, sess_uuid),
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
# chats.seq → ChatMessageData.index 로 매핑. indicator_value(Decimal) → float.
|
|
# 말풍선 텍스트는 chats.meta.script 에 영속화돼 있어 그대로 꺼낸다(프론트 하드코딩 X).
|
|
res.messages = [
|
|
ChatMessageData(
|
|
chat_id=r.chat_id,
|
|
session_id=r.session_id,
|
|
card_id=r.card_id,
|
|
index=r.seq,
|
|
sender=r.sender,
|
|
target_price=r.target_price,
|
|
card_used_yn=r.card_used_yn,
|
|
indicator_value=float(r.indicator_value) if r.indicator_value is not None else None,
|
|
card_type=r.card_type,
|
|
script=(r.meta or {}).get("script"),
|
|
step=(r.meta or {}).get("step"),
|
|
)
|
|
for r in rows
|
|
]
|
|
return res
|
|
|
|
async def list_cards(self, qt_id: str, company_id=None) -> Res_QuotationCards:
|
|
res = Res_QuotationCards()
|
|
qt_uuid = uuid.UUID(qt_id)
|
|
err_type, quotation = await self._fetch(qt_uuid, company_id)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
# 견적의 버전(quotation.version_id)에 묶인 카드를 조회한다(version_nego_cards/version_wild_cards).
|
|
err_type, rows = await DB_SESSION_MNG.execute_lambda(
|
|
quotations.DBType(),
|
|
DBWRType.DB_READ.value,
|
|
lambda s: self.quotation_crud.get_version_cards(s, quotation.version_id),
|
|
)
|
|
if err_type != ErrorType.SUCCESS:
|
|
res.result.SetResult(err_type)
|
|
return res
|
|
|
|
res.qt_id = quotation.qt_id
|
|
# rows = [(card_type, card_pk, number, name, script, edit_script, condition, memo), ...].
|
|
cards = []
|
|
for card_type, card_pk, number, name, script, edit, condition, memo in rows:
|
|
is_wild = card_type == 2
|
|
cards.append(
|
|
QuotationCardData(
|
|
session_card_id=card_pk,
|
|
qt_id=quotation.qt_id,
|
|
nego_card_id=None if is_wild else card_pk,
|
|
wild_card_id=card_pk if is_wild else None,
|
|
type=card_type,
|
|
number=number,
|
|
name=name,
|
|
script=script,
|
|
edit_script=edit,
|
|
condition=condition,
|
|
memo=memo,
|
|
)
|
|
)
|
|
res.cards = cards
|
|
return res
|