- 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>
176 lines
16 KiB
Python
176 lines
16 KiB
Python
from sqlalchemy.orm import declarative_base
|
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, SmallInteger, BigInteger
|
|
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
|
from sqlalchemy.sql import text
|
|
|
|
from common.enums import DBType
|
|
|
|
# 모든 ORM 모델의 베이스. insert 시 isinstance 체크에도 사용된다.
|
|
MAIN_BASE = declarative_base()
|
|
|
|
|
|
class supplier_users(MAIN_BASE):
|
|
# 이 프로젝트의 기본 유저. 실제 테이블은 negosium_db 의 supplier 스키마(supplier_users).
|
|
@staticmethod
|
|
def DBType():
|
|
return DBType.USER.value
|
|
|
|
__tablename__ = "supplier_users"
|
|
__table_args__ = {"schema": "supplier"}
|
|
|
|
# gen_random_uuid() 는 pgcrypto 확장 기준. 코드값(status/role/type)은 SMALLINT 정수 코드(앱 enum 매핑).
|
|
su_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 유저 식별자(PK)
|
|
supplier_id = Column(UUID(as_uuid=True), nullable=False) # 소속 공급사(partner.suppliers.supplier_id)
|
|
id = Column(String(20), nullable=False) # 로그인 ID
|
|
password = Column(String(255), nullable=False) # 해시된 비밀번호이어야 함
|
|
name = Column(String(50), nullable=True) # 이름
|
|
email = Column(String(255), nullable=True) # 이메일
|
|
contact_number = Column(String(20), nullable=True) # 연락처
|
|
last_accessed_at = Column(DateTime(timezone=True), nullable=False) # 마지막 접속 시각
|
|
status = Column(SmallInteger, nullable=False, server_default=text("1")) # 상태: 1=active, 2=inactive
|
|
role = Column(SmallInteger, nullable=False, server_default=text("1")) # 권한: 1=user, 2=manager
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
|
|
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
|
|
|
|
|
|
class suppliers(MAIN_BASE):
|
|
# partner.suppliers (공급사 회사). 공급사명(name) 조회용. partner 도메인이라 DBType 은 PARTNER.
|
|
@staticmethod
|
|
def DBType():
|
|
return DBType.PARTNER.value
|
|
|
|
__tablename__ = "suppliers"
|
|
__table_args__ = {"schema": "partner"}
|
|
|
|
supplier_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 공급사 식별자(PK)
|
|
company_id = Column(UUID(as_uuid=True), nullable=False) # 소속 회사(company.companies.company_id)
|
|
user_id = Column(UUID(as_uuid=True), nullable=False) # 등록 유저(company.users.user_id)
|
|
name = Column(String(100), nullable=False) # 공급사명
|
|
code = Column(String(20), nullable=True) # 공급사 코드
|
|
manager_name = Column(String(50), nullable=True) # 담당자명
|
|
manager_email = Column(String(255), nullable=True) # 담당자 이메일
|
|
manager_contact_number = Column(String(20), nullable=True) # 담당자 연락처
|
|
priority = Column(String(10), nullable=True) # 우선순위 (고객사별 문자열 값 가능)
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
|
|
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
|
|
|
|
|
|
class items(MAIN_BASE):
|
|
# partner.items (상품).
|
|
@staticmethod
|
|
def DBType():
|
|
return DBType.PARTNER.value
|
|
|
|
__tablename__ = "items"
|
|
__table_args__ = {"schema": "partner"}
|
|
|
|
item_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 상품 식별자(PK)
|
|
company_id = Column(UUID(as_uuid=True), nullable=False) # 소속 회사(company.companies.company_id)
|
|
user_id = Column(UUID(as_uuid=True), nullable=False) # 등록 유저(company.users.user_id)
|
|
name = Column(String(100), nullable=False) # 상품명
|
|
code = Column(String(30), nullable=True) # 상품 코드
|
|
price = Column(BigInteger, nullable=True) # 가격(원)
|
|
category = Column(String(255), nullable=True) # 카테고리
|
|
image_url = Column(String(255), nullable=True) # 이미지 URL
|
|
model_name = Column(String(100), nullable=True) # 모델명
|
|
spec = Column(String(255), nullable=True) # 규격
|
|
moq = Column(String(50), nullable=True) # 최소 주문 수량
|
|
lead_time = Column(SmallInteger, nullable=True) # 배송 리드타임
|
|
manufacturer = Column(String(50), nullable=True) # 제조사
|
|
made_in = Column(String(100), nullable=True) # 원산지
|
|
quantity_unit = Column(SmallInteger, nullable=True) # 취급 단위 (코드, 앱 enum 매핑)
|
|
delivery_type = Column(SmallInteger, nullable=True) # 배송 유형 (코드, 앱 enum 매핑)
|
|
vat_yn = Column(Boolean, nullable=True) # 부가세 포함 여부
|
|
delivery_fee_yn = Column(Boolean, nullable=True) # 배송비 포함 여부
|
|
internet_lowest_price_yn = Column(Boolean, nullable=False, server_default=text("false")) # 최저가 솔루션 보조 컬럼
|
|
category_type = Column(Integer, nullable=False, server_default=text("1")) # 카테고리 조회용 자동 증가 숫자
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
|
|
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
|
|
|
|
|
|
class sessions(MAIN_BASE):
|
|
# negotiation.sessions (협상 세션).
|
|
@staticmethod
|
|
def DBType():
|
|
return DBType.NEGOTIATION.value
|
|
|
|
__tablename__ = "sessions"
|
|
__table_args__ = {"schema": "negotiation"}
|
|
|
|
session_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 협상 세션 식별자(PK)
|
|
quotation_id = Column(UUID(as_uuid=True), nullable=False) # 소속 견적(quotation.quotations.qt_id)
|
|
item_id = Column(UUID(as_uuid=True), nullable=False) # 대상 상품(partner.items.item_id)
|
|
supplier_id = Column(UUID(as_uuid=True), nullable=False) # 대상 공급사(partner.suppliers.supplier_id)
|
|
qt_number = Column(String(30), nullable=False) # 견적번호(스냅샷)
|
|
qt_round = Column(Integer, nullable=False) # 견적 라운드(스냅샷)
|
|
qt_type = Column(SmallInteger, nullable=False) # 견적 유형: 1=재협상, 2=재견적 (QtType)
|
|
target_price = Column(BigInteger, nullable=False) # 목표가(원)
|
|
status = Column(SmallInteger, nullable=False) # 진행 상태 (SessionStatus 코드)
|
|
bid_price = Column(BigInteger, nullable=True) # 입찰가(원)
|
|
bid_at = Column(DateTime(timezone=True), nullable=True) # 입찰 시각
|
|
end_time = Column(DateTime(timezone=True), nullable=False) # 세션 종료(마감) 시각
|
|
reject_reason = Column(String(255), nullable=True) # 거절 사유
|
|
reject_price = Column(BigInteger, nullable=True) # 거절 시 제시가(원)
|
|
reject_delivery_type = Column(SmallInteger, nullable=True) # 거절 시 배송 유형 (코드)
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
|
|
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
|
|
|
|
|
|
class quotations(MAIN_BASE):
|
|
# quotation.quotations (견적).
|
|
@staticmethod
|
|
def DBType():
|
|
return DBType.QUOTATION.value
|
|
|
|
__tablename__ = "quotations"
|
|
__table_args__ = {"schema": "quotation"}
|
|
|
|
qt_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 견적 식별자(PK)
|
|
user_id = Column(UUID(as_uuid=True), nullable=False) # 생성 유저(company.users.user_id)
|
|
qt_setting_id = Column(UUID(as_uuid=True), nullable=False) # 견적 설정(quotation.quotation_settings.qt_setting_id)
|
|
version_id = Column(UUID(as_uuid=True), nullable=False) # 버전(card.versions.version_id)
|
|
name = Column(String(50), nullable=False) # 견적명
|
|
number = Column(String(30), nullable=False) # 견적번호
|
|
type = Column(SmallInteger, nullable=False) # 견적 유형: 1=재협상, 2=재견적 (QtType)
|
|
round = Column(Integer, nullable=False, server_default=text("1")) # 재견적 회차
|
|
status = Column(SmallInteger, nullable=False) # 진행 상태 (QuotationStatus 코드)
|
|
start_time = Column(DateTime(timezone=True), nullable=False) # 견적 시작 시각
|
|
end_time = Column(DateTime(timezone=True), nullable=False) # 견적 종료(마감) 시각
|
|
manager_name = Column(String(50), nullable=True) # 담당자명
|
|
manager_email = Column(String(255), nullable=True) # 담당자 이메일
|
|
manager_contact_number = Column(String(20), nullable=True) # 담당자 연락처
|
|
memo = Column(String(100), nullable=True) # 메모
|
|
iteration = Column(Integer, nullable=False, server_default=text("0")) # 반복 횟수
|
|
preferred_sp_yn = Column(Boolean, nullable=True) # 선호 공급사 지정 여부
|
|
preferred_sp_id = Column(UUID(as_uuid=True), nullable=True) # 선호 공급사(partner.suppliers.supplier_id)
|
|
preferred_sp_name = Column(String(20), nullable=True) # 선호 공급사명(스냅샷)
|
|
equal_bid_yn = Column(Boolean, nullable=True) # 동일가 입찰 발생 여부
|
|
equal_bid_data = Column(JSONB, nullable=True) # 동일가 입찰 상세(JSON)
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
|
|
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
|
|
|
|
|
|
class supplier_user_tokens(MAIN_BASE):
|
|
# 유저 인증 토큰. supplier_users 1 : N tokens.
|
|
@staticmethod
|
|
def DBType():
|
|
return DBType.USER.value
|
|
|
|
__tablename__ = "supplier_user_tokens"
|
|
__table_args__ = {"schema": "supplier"}
|
|
|
|
sut_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) # 토큰 식별자(PK)
|
|
su_id = Column(UUID(as_uuid=True), nullable=False) # 소유 유저(supplier.supplier_users.su_id)
|
|
type = Column(SmallInteger, nullable=False) # 토큰 종류 (코드, 앱 enum 매핑)
|
|
token = Column(JSONB, nullable=False) # 토큰 본문(JSON)
|
|
issued_at = Column(DateTime(timezone=True), nullable=False) # 발급 시각
|
|
expired_at = Column(DateTime(timezone=True), nullable=False) # 만료 시각
|
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC)
|
|
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신)
|
|
deleted = Column(Boolean, nullable=False, server_default=text("false")) # 소프트 삭제 여부
|