from abc import ABC, abstractmethod from typing import Tuple from sqlalchemy import and_, select, update from sqlalchemy.ext.asyncio import AsyncSession from common.database.db_session_manager import DB_SESSION_MNG from common.database.model.models import place_faqs from common.enums import ErrorType, FactStatus, SourceType from common.logger import LOG from common.utils.gtime import GTime _PUBLISHABLE = (FactStatus.VERIFIED.value, FactStatus.CORRECTED.value) _ACTIVE = (FactStatus.UNVERIFIED.value, FactStatus.PENDING_OWNER.value) + _PUBLISHABLE # FAQ CRUD. class IFaqCRUD(ABC): @abstractmethod async def list_faqs(self, cdb: AsyncSession, place_id, publishable_only: bool) -> Tuple[ErrorType, list]: pass @abstractmethod async def add_faq(self, cdb: AsyncSession, faq: place_faqs) -> ErrorType: pass @abstractmethod async def expire_generated(self, cdb: AsyncSession, place_id, ts) -> Tuple[ErrorType, int]: pass @abstractmethod async def transition(self, cdb: AsyncSession, faq_id, from_statuses, to_status, data: dict) -> Tuple[ErrorType, int]: pass @abstractmethod async def get_faq(self, cdb: AsyncSession, place_id, faq_id) -> Tuple[ErrorType, place_faqs]: pass class FaqCRUD(IFaqCRUD): async def list_faqs(self, cdb: AsyncSession, place_id, publishable_only: bool = False) -> Tuple[ErrorType, list]: try: conds = [place_faqs.place_id == place_id, place_faqs.deleted == False] # noqa: E712 conds.append(place_faqs.status.in_(_PUBLISHABLE if publishable_only else _ACTIVE)) query = select(place_faqs).where(and_(*conds)).order_by(place_faqs.sort_order.asc(), place_faqs.created_at.asc()) err_type, rows = await DB_SESSION_MNG.execute(cdb, query) return (err_type, list(rows) if err_type == ErrorType.SUCCESS else []) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, [] async def get_faq(self, cdb: AsyncSession, place_id, faq_id) -> Tuple[ErrorType, place_faqs]: try: query = select(place_faqs).where( place_faqs.faq_id == faq_id, place_faqs.place_id == place_id, place_faqs.deleted == False # noqa: E712 ).limit(1) err_type, rows = await DB_SESSION_MNG.execute(cdb, query) if err_type != ErrorType.SUCCESS: return err_type, None if len(rows) != 1: return ErrorType.DB_INVALID_KEY, None return ErrorType.SUCCESS, rows[0] except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, None async def add_faq(self, cdb: AsyncSession, faq: place_faqs) -> ErrorType: try: return await DB_SESSION_MNG.insert(cdb, faq) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED async def expire_generated(self, cdb: AsyncSession, place_id, ts) -> Tuple[ErrorType, int]: """재생성 전에 **LLM 이 쓴** FAQ 를 내린다.""" try: query = ( update(place_faqs) .where( place_faqs.place_id == place_id, place_faqs.deleted == False, # noqa: E712 # 목표 수를 채운 공통 질문(TEMPLATE)도 자동 산출물이다 — 재생성마다 다시 고른다. place_faqs.generated_by.in_((SourceType.LLM.value, SourceType.TEMPLATE.value)), # 이미 내려간 것(EXPIRED)과 사장님이 반려한 것(REJECTED)은 그대로 둔다 — 반려는 판단의 기록이라 재생성이 지울 이유가 없다. place_faqs.status.not_in((FactStatus.EXPIRED.value, FactStatus.REJECTED.value)), ) .values(status=FactStatus.EXPIRED.value, updated_at=ts) ) return await DB_SESSION_MNG.add_with_rowcount(cdb, query) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, 0 async def transition(self, cdb: AsyncSession, faq_id, from_statuses, to_status: int, data: dict) -> Tuple[ErrorType, int]: try: query = ( update(place_faqs) .where(place_faqs.faq_id == faq_id, place_faqs.status.in_(tuple(from_statuses)), place_faqs.deleted == False) # noqa: E712 .values(status=to_status, updated_at=GTime.UTC(), **data) ) return await DB_SESSION_MNG.add_with_rowcount(cdb, query) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, 0