"""견적 조회(목록·상세·상태·결과·세션·채팅·카드)·삭제와 공용 단건조회(_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