협력사가 단종·품절을 대화 도중 알아채도 봇의 결렬 선언을 기다려야 했고, 거부로 끝난 협상은 가격을 남겨도 낙찰 후보에서 빠져 계약으로 이어지지 않았다. negosium - 채팅 액션바에 협상 거부 진입점 — 대화가 끝나지 않고 입력을 기다리는 동안만 노출, 주 CTA 와 붙지 않게 넓은 화면은 우측 끝 고정·좁은 화면은 wrap - 거부 팝업은 목록 거부 팝업과 같은 어휘·규격, 대화 중이라 공급 희망 가격·의견을 더 받는다 - /reject 에 reject_price·opinion 추가 — sessions.reject_price 저장, 의견은 custom 병합 - 화면 문구 '거절' → '거부' 통일 (버튼·배지·탭·토스트·안내 팝업) negodata - 개찰 견적 직접 낙찰 후보 = 가격을 써낸 세션 — 투찰한 협상완료 + 공급 희망가를 남긴 협상거부 - 계약가 파생 _award_price/awardPrice — coalesce(투찰가, 거부 시 공급 희망가) - 통계 낙찰 세션 조인도 같은 기준 — 안 고치면 거부가로 낙찰한 건이 절감 집계에서 빠진다 - 세션 상태 탭 라벨 '거절사유/거절가격/거절배송방식' → '거부…' 자동 마감 판정(close_and_decide)은 그대로 — 자동 낙찰은 투찰가만 본다.
130 lines
6.0 KiB
Python
130 lines
6.0 KiB
Python
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Path, Query
|
|
from fastapi.security import HTTPAuthorizationCredentials
|
|
|
|
from common.models.gmodel import UserInfo
|
|
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse, security
|
|
from services.negotiation_service import NegotiationService
|
|
from .protocol import (
|
|
Req_ExtraInfo,
|
|
Req_Reject,
|
|
Req_Renegotiation,
|
|
Res_ExtraInfo,
|
|
Res_Participate,
|
|
Res_Reject,
|
|
Res_Renegotiation,
|
|
Res_SessionList,
|
|
)
|
|
|
|
router = APIRouter(prefix="/v1/negotiation", tags=["Negotiation"], responses={404: {"description": "Not found"}})
|
|
|
|
|
|
@router.get(
|
|
path="/sessions",
|
|
response_model=Res_SessionList,
|
|
summary="협상 세션 목록",
|
|
description="로그인한 공급사의 협상 세션 목록. 필터(status/qt_type, 정수 코드)·페이지네이션 지원. 기본 정렬(order 미지정)은 '할 일(협상생성·협상중) 우선 + 마감 임박순', 종료(완료·미참여·거부)는 하단·최근순. order 를 주면 그룹 없이 전체 마감순으로 정렬.",
|
|
)
|
|
async def list_sessions(
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
service: NegotiationService = Depends(),
|
|
status: Optional[int] = Query(None, description="세션 상태 코드 (SessionStatus)"),
|
|
qt_type: Optional[int] = Query(None, description="견적 유형 코드 (QtType: 1=재협상, 2=재견적, 3=신규협상, 4=신규견적)"),
|
|
order: Optional[str] = Query(None, description="마감일 전체 정렬: asc(임박순)/desc(여유순). 미지정 시 기본 그룹 정렬('할 일' 우선 → 종료는 하단·최근순). 지정하면 그룹 없이 전체를 마감 기준으로 정렬."),
|
|
page: int = Query(1, ge=1, description="페이지 (1부터)"),
|
|
page_size: int = Query(20, ge=1, le=100, description="페이지당 건수 (1~100)"),
|
|
keyword: Optional[str] = Query(None, description="검색어 — 견적번호·상품명·상품코드 부분일치(대소문자 무시)"),
|
|
result: Optional[int] = Query(None, description="결과 필터(SessionResult): 1=낙찰 2=미낙찰 3=결렬(개찰). 미지정 시 전체"),
|
|
):
|
|
return RemoveNoneResponse(
|
|
await service.list_sessions(user_info, credentials.credentials, status, qt_type, order, page, page_size, keyword, result)
|
|
)
|
|
|
|
|
|
@router.post(
|
|
path="/sessions/{session_id}/participate",
|
|
response_model=Res_Participate,
|
|
summary="협상 참여",
|
|
description="세션에 참여한다. 소유(공급사)·세션상태·견적마감·마감시간 검증 후 협상생성→협상중, 견적→견적진행중으로 전이.",
|
|
)
|
|
async def participate(
|
|
session_id: str = Path(description="대상 협상 세션 uuid"),
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
service: NegotiationService = Depends(),
|
|
):
|
|
return RemoveNoneResponse(await service.participate(user_info, credentials.credentials, session_id))
|
|
|
|
|
|
@router.post(
|
|
path="/sessions/{session_id}/reject",
|
|
response_model=Res_Reject,
|
|
summary="협상 거부",
|
|
description="세션 참여를 거부하거나 진행 중인 협상을 거부한다. 소유(공급사)·세션상태(완료/미참여/거부 불가)·견적마감·마감시간 검증 후 협상거부로 전이하고 사유·공급 희망 가격·의견을 저장.",
|
|
)
|
|
async def reject(
|
|
session_id: str = Path(description="대상 협상 세션 uuid"),
|
|
req: Req_Reject = ...,
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
service: NegotiationService = Depends(),
|
|
):
|
|
return RemoveNoneResponse(
|
|
await service.reject(
|
|
user_info, credentials.credentials, session_id, req.reject_reason, req.reject_price, req.opinion,
|
|
)
|
|
)
|
|
|
|
|
|
@router.post(
|
|
path="/sessions/{session_id}/extra-info",
|
|
response_model=Res_ExtraInfo,
|
|
summary="협상완료 부가정보 저장",
|
|
description="협상 타결(완료) 세션에 부가정보(표준납기/MOQ/발주배수/배송유형 등, 회사 정의 session_fields)를 저장한다. 본인 공급사의 완료 세션만 허용.",
|
|
)
|
|
async def save_extra_info(
|
|
session_id: str = Path(description="대상 협상 세션 uuid"),
|
|
req: Req_ExtraInfo = ...,
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
service: NegotiationService = Depends(),
|
|
):
|
|
return RemoveNoneResponse(await service.save_extra_info(user_info, credentials.credentials, session_id, req))
|
|
|
|
|
|
@router.post(
|
|
path="/session/{session_id}/renegotiation",
|
|
response_model=Res_Renegotiation,
|
|
summary="재협상 요청",
|
|
description="낙찰 없이 마감된(개찰) 건에 대해 공급사가 재협상을 요청한다. 담당자 승인 시 다음 라운드가 생성된다. 본인 공급사의 마지막 라운드 세션만 허용.",
|
|
)
|
|
async def request_renegotiation(
|
|
req: Req_Renegotiation,
|
|
session_id: str = Path(..., description="협상 세션 uuid"),
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
service: NegotiationService = Depends(),
|
|
):
|
|
return RemoveNoneResponse(
|
|
await service.request_renegotiation(user_info, credentials.credentials, session_id, req)
|
|
)
|
|
|
|
|
|
@router.delete(
|
|
path="/session/{session_id}/renegotiation",
|
|
response_model=Res_Renegotiation,
|
|
summary="재협상 요청 철회",
|
|
description="심사 대기(PENDING) 중인 본인 요청을 철회한다.",
|
|
)
|
|
async def cancel_renegotiation(
|
|
session_id: str = Path(..., description="협상 세션 uuid"),
|
|
user_info: UserInfo = Depends(IsValidAccessToken),
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
service: NegotiationService = Depends(),
|
|
):
|
|
return RemoveNoneResponse(
|
|
await service.cancel_renegotiation(user_info, credentials.credentials, session_id)
|
|
)
|