from abc import ABC, abstractmethod from typing import Optional, Tuple from sqlalchemy import String, and_, cast, func, select, text, update from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import aliased from common.database.db_session_manager import DB_SESSION_MNG from common.database.model.models import items, quotations, sessions, suppliers, users from common.enums import ErrorType from common.logger import LOG # 재협상 요청은 전용 테이블 없이 sessions.custom.renegotiation 에 들어간다(IMK #15). # 조회는 세션을 견적·상품·공급사와 조인하면서 JSONB 조건으로 거른다. _RENEGO = sessions.custom["renegotiation"] class IRenegotiationCRUD(ABC): @abstractmethod async def list_requests(self, cdb: AsyncSession, company_id, status: Optional[int], skip: int, limit: int, owner_user_id=None) -> Tuple[ErrorType, list, int]: pass @abstractmethod async def get_request(self, cdb: AsyncSession, company_id, session_id) -> Tuple[ErrorType, Optional[tuple]]: pass @abstractmethod async def merge_custom(self, cdb: AsyncSession, session_id, patch: dict) -> ErrorType: pass class RenegotiationCRUD(IRenegotiationCRUD): @staticmethod def _base_query(company_id, status: Optional[int], owner_user_id=None): # 회사 스코프는 견적 작성자(users.company_id)로 건다 — quotations 에 company_id 컬럼이 없다. # owner_user_id 가 오면(일반관리자) 자기 견적만 — 본인 견적의 재협상 요청만 보고 처리한다. OWNER 는 None(회사 전체). conds = [ sessions.deleted == False, # noqa: E712 users.company_id == company_id, _RENEGO.isnot(None), ] if owner_user_id is not None: conds.append(quotations.user_id == owner_user_id) if status is not None: conds.append(cast(_RENEGO["status"].astext, String) == str(status)) return and_(*conds) @staticmethod def _select(): # 승인/반려한 담당자 이름 — custom.renegotiation.decided_by(user_id) 로 users 를 한 번 더(별칭) 조인. decider = aliased(users) return ( select( sessions.session_id, sessions.quotation_id, sessions.supplier_id, sessions.target_price, sessions.bid_price, sessions.custom, quotations.number, quotations.round, quotations.name, quotations.close_reason, items.name, suppliers.name, decider.name, users.user_id, # 견적 작성자(소유자) — 승인/반려 소유권 게이팅용 (row[13]) users.name, # 견적 담당자(작성자) 이름 — 리스트 표시용 (row[14]) sessions.item_id, # 상품 링크용 (row[15]) ) .select_from(sessions) .join(quotations, quotations.qt_id == sessions.quotation_id) .join(users, users.user_id == quotations.user_id) .outerjoin(items, items.item_id == sessions.item_id) .outerjoin(suppliers, suppliers.supplier_id == sessions.supplier_id) .outerjoin(decider, cast(decider.user_id, String) == _RENEGO["decided_by"].astext) ) async def list_requests(self, cdb: AsyncSession, company_id, status: Optional[int], skip: int, limit: int, owner_user_id=None) -> Tuple[ErrorType, list, int]: try: where = self._base_query(company_id, status, owner_user_id) cnt_err, cnt_rows = await DB_SESSION_MNG.execute( cdb, select(func.count()) .select_from(sessions) .join(quotations, quotations.qt_id == sessions.quotation_id) .join(users, users.user_id == quotations.user_id) .where(where), ) if cnt_err != ErrorType.SUCCESS: return cnt_err, [], 0 total = int(cnt_rows[0] or 0) if cnt_rows else 0 # 요청 시각 내림차순 — JSONB 텍스트지만 ISO8601 이라 사전순 = 시간순. err, rows = await DB_SESSION_MNG.execute( cdb, self._select().where(where).order_by(_RENEGO["requested_at"].astext.desc()).offset(skip).limit(limit), ) if err != ErrorType.SUCCESS: return err, [], 0 return ErrorType.SUCCESS, list(rows), total except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, [], 0 async def get_request(self, cdb: AsyncSession, company_id, session_id) -> Tuple[ErrorType, Optional[tuple]]: try: err, rows = await DB_SESSION_MNG.execute( cdb, self._select().where(and_(self._base_query(company_id, None), sessions.session_id == session_id)).limit(1), ) if err != ErrorType.SUCCESS: return err, None return ErrorType.SUCCESS, rows[0] if rows else None except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, None async def merge_custom(self, cdb: AsyncSession, session_id, patch: dict) -> ErrorType: # 부가정보와 같은 컬럼을 쓰므로 통째로 덮지 않고 병합한다. try: query = ( update(sessions) .where(sessions.session_id == session_id) .values(custom=func.coalesce(sessions.custom, cast(text("'{}'"), JSONB)).op("||")(cast(patch, JSONB))) ) return await DB_SESSION_MNG.add(cdb, query) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED