- GET /v1/negotiation/sessions: 로그인 공급사의 세션 목록(필터/정렬/페이지네이션)
· qt_end_time 은 견적(quotation.end_time) 기준, sessions⨝items⨝quotations 조인
· status/qt_type 은 정수 코드로 응답(라벨 매핑은 프론트)
- POST /v1/negotiation/sessions/{session_id}/participate: 협상 참여
· 검증: 소유(공급사 대조)→세션상태→견적마감→마감시간, 에러코드 1300~1304
· 협상생성→협상중, 견적→견적진행중 (협상중/완료는 무변경 진입)
· 마감초과 시 협상생성 세션만 미참여로 정리
- DBType.NEGOTIATION/QUOTATION, items/sessions/quotations 모델
- QtType/SessionStatus/QuotationStatus enum, AuthService.authenticate 공통화
- 협상 e2e 테스트(test_negotiation.py)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
141 lines
6.3 KiB
Python
141 lines
6.3 KiB
Python
from abc import ABC, abstractmethod
|
|
from typing import Tuple
|
|
|
|
from sqlalchemy import asc, desc, func, select, update
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.database.model.models import items, quotations, sessions
|
|
from common.enums import ErrorType
|
|
from common.logger import LOG
|
|
|
|
|
|
# 협상 세션 CRUD. 목록은 세션(negotiation) ⨝ 상품(partner) ⨝ 견적(quotation) 조인으로 만든다.
|
|
# 마감일(qt_end_time)은 견적(quotation.end_time)이 진실값이다(session.end_time 은 협상 종료 시점 기록용).
|
|
class ISessionCRUD(ABC):
|
|
@abstractmethod
|
|
async def list_by_supplier(self, cdb: AsyncSession, supplier_id, status, qt_type, order, offset, limit) -> Tuple[ErrorType, list]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def count_by_supplier(self, cdb: AsyncSession, supplier_id, status, qt_type) -> Tuple[ErrorType, int]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def get_session_by_id(self, cdb: AsyncSession, session_id) -> Tuple[ErrorType, sessions]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def get_quotation_by_id(self, cdb: AsyncSession, quotation_id) -> Tuple[ErrorType, quotations]:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def update_session_status(self, cdb: AsyncSession, session_id, status: int) -> ErrorType:
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def update_quotation_status(self, cdb: AsyncSession, quotation_id, status: int) -> ErrorType:
|
|
pass
|
|
|
|
|
|
class SessionCRUD(ISessionCRUD):
|
|
@staticmethod
|
|
def __filters(supplier_id, status, qt_type):
|
|
conds = [sessions.supplier_id == supplier_id, sessions.deleted == False] # noqa: E712
|
|
if status is not None:
|
|
conds.append(sessions.status == status)
|
|
if qt_type is not None:
|
|
conds.append(sessions.qt_type == qt_type)
|
|
return conds
|
|
|
|
async def list_by_supplier(self, cdb: AsyncSession, supplier_id, status, qt_type, order, offset, limit) -> Tuple[ErrorType, list]:
|
|
try:
|
|
conds = self.__filters(supplier_id, status, qt_type)
|
|
order_col = desc(quotations.end_time) if order == "desc" else asc(quotations.end_time)
|
|
query = (
|
|
select(
|
|
sessions.session_id,
|
|
sessions.status,
|
|
sessions.qt_type,
|
|
sessions.qt_number,
|
|
quotations.end_time, # qt_end_time = 견적 마감 시각
|
|
items.code,
|
|
items.name,
|
|
items.model_name,
|
|
items.manufacturer,
|
|
)
|
|
.join(items, items.item_id == sessions.item_id)
|
|
.join(quotations, quotations.qt_id == sessions.quotation_id)
|
|
.where(*conds, items.deleted == False, quotations.deleted == False) # noqa: E712
|
|
.order_by(order_col)
|
|
.offset(offset)
|
|
.limit(limit)
|
|
)
|
|
err_type, rows = await DB_SESSION_MNG.execute(cdb, query, "list_by_supplier failed.")
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, []
|
|
return ErrorType.SUCCESS, rows
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, []
|
|
|
|
async def count_by_supplier(self, cdb: AsyncSession, supplier_id, status, qt_type) -> Tuple[ErrorType, int]:
|
|
try:
|
|
conds = self.__filters(supplier_id, status, qt_type)
|
|
query = (
|
|
select(func.count())
|
|
.select_from(sessions)
|
|
.join(items, items.item_id == sessions.item_id)
|
|
.join(quotations, quotations.qt_id == sessions.quotation_id)
|
|
.where(*conds, items.deleted == False, quotations.deleted == False) # noqa: E712
|
|
)
|
|
err_type, rows = await DB_SESSION_MNG.execute(cdb, query, "count_by_supplier failed.")
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, 0
|
|
return ErrorType.SUCCESS, (rows[0] if rows else 0)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, 0
|
|
|
|
async def get_session_by_id(self, cdb: AsyncSession, session_id) -> Tuple[ErrorType, sessions]:
|
|
try:
|
|
query = select(sessions).where(sessions.session_id == session_id, sessions.deleted == False).limit(1) # noqa: E712
|
|
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query, f"get_session_by_id({session_id}) failed.")
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, None
|
|
if len(row_list) != 1:
|
|
return ErrorType.DB_INVALID_KEY, None
|
|
return ErrorType.SUCCESS, row_list[0]
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, None
|
|
|
|
async def get_quotation_by_id(self, cdb: AsyncSession, quotation_id) -> Tuple[ErrorType, quotations]:
|
|
try:
|
|
query = select(quotations).where(quotations.qt_id == quotation_id, quotations.deleted == False).limit(1) # noqa: E712
|
|
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query, f"get_quotation_by_id({quotation_id}) failed.")
|
|
if err_type != ErrorType.SUCCESS:
|
|
return err_type, None
|
|
if len(row_list) != 1:
|
|
return ErrorType.DB_INVALID_KEY, None
|
|
return ErrorType.SUCCESS, row_list[0]
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED, None
|
|
|
|
async def update_session_status(self, cdb: AsyncSession, session_id, status: int) -> ErrorType:
|
|
try:
|
|
query = update(sessions).where(sessions.session_id == session_id).values(status=status)
|
|
return await DB_SESSION_MNG.add(cdb, query)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED
|
|
|
|
async def update_quotation_status(self, cdb: AsyncSession, quotation_id, status: int) -> ErrorType:
|
|
try:
|
|
query = update(quotations).where(quotations.qt_id == quotation_id).values(status=status)
|
|
return await DB_SESSION_MNG.add(cdb, query)
|
|
except Exception as ex:
|
|
LOG.e_no_callstack(ex)
|
|
return ErrorType.DB_RUN_FAILED
|