- test DB 세션마다 자동 create/drop (팀원은 Postgres만 있으면 pytest 한 방) - auth_headers 시드 픽스처(무인증 /auth/create 제거 대응) + other_company_id - 커버: 회사 스코프(견적·상품·협력사·대시보드·세팅), 견적 마감 재견적 O/X + 알림, 견적 생성·목표가, 알림함 읽기, 회사유저 OWNER 게이팅, 기존 파일 검증/기대결과 주석 정비 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
4.2 KiB
Python
99 lines
4.2 KiB
Python
"""견적 생성 — item×supplier 조합마다 세션이 생기고, 목표가가 산정되는지 검증.
|
||
|
||
기존 test_features.test_quotation_create 는 item/supplier 없이 '세션 0건' 경로만 본다.
|
||
여기선 상품(인터넷최저가)을 시드해 세션 생성 + 목표가 계산(신규=인터넷최저가×(1−수수료))까지 본다.
|
||
서비스(create_quotation)를 직접 호출한다 — HTTP/auth 경로(현재 /v1/auth/create 미존재)를 안 타고 생성 로직만 격리.
|
||
"""
|
||
import uuid
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import text
|
||
|
||
from common.enums import QuotationType
|
||
from crud.quotation_crud import QuotationCRUD
|
||
from router.v1.quotation.protocol import Req_CreateQuotation
|
||
from services.quotation_service import QuotationService
|
||
|
||
FUTURE = datetime(2999, 1, 1) # 마감시각 미래 — 생성 직후 크론에 안 잡히게
|
||
|
||
|
||
async def test_create_builds_sessions_with_target_price(db_engine, company_id):
|
||
"""검증: 신규견적을 상품2×공급사2로 생성.
|
||
기대결과: success=True, 세션 4개, 각 목표가 = int(인터넷최저가 × (1−0.078))."""
|
||
item1 = await _seed_item(db_engine, company_id, internet_lowest=100_000)
|
||
item2 = await _seed_item(db_engine, company_id, internet_lowest=50_000)
|
||
suppliers = [uuid.uuid4(), uuid.uuid4()]
|
||
|
||
req = Req_CreateQuotation(
|
||
qt_setting_id=uuid.uuid4(), # FK 미설정 — 세팅 없으면 율 0(신규는 인터넷최저가만 쓰므로 무관)
|
||
name="신규견적A",
|
||
type=QuotationType.NEW_QUOTE.value,
|
||
end_time=FUTURE,
|
||
item_ids=[item1, item2],
|
||
supplier_ids=suppliers,
|
||
)
|
||
res = await _service().create_quotation(str(uuid.uuid4()), req)
|
||
|
||
assert res.result.success is True
|
||
assert res.session_count == 4 # 상품 2 × 공급사 2
|
||
|
||
fee = QuotationService.INTERNET_AVERAGE_FEE
|
||
expected = {item1: int(100_000 * (1 - fee)), item2: int(50_000 * (1 - fee))}
|
||
rows = await _session_target_prices(db_engine, res.qt_id)
|
||
assert len(rows) == 4
|
||
for item_id, target_price in rows:
|
||
assert target_price == expected[item_id] # 상품별 목표가가 공급사 수만큼 동일
|
||
|
||
|
||
async def test_create_without_price_fails(db_engine, company_id):
|
||
"""검증: 가격 후보(인터넷최저가·md 등)가 전무한 상품으로 견적 생성.
|
||
기대결과: 목표가 산정 불가로 success=False, 세션 0건(미생성)."""
|
||
item = await _seed_item(db_engine, company_id, internet_lowest=None)
|
||
|
||
req = Req_CreateQuotation(
|
||
qt_setting_id=uuid.uuid4(),
|
||
name="가격없음",
|
||
type=QuotationType.NEW_QUOTE.value,
|
||
end_time=FUTURE,
|
||
item_ids=[item],
|
||
supplier_ids=[uuid.uuid4()],
|
||
)
|
||
res = await _service().create_quotation(str(uuid.uuid4()), req)
|
||
|
||
assert res.result.success is False # QUOTATION_TARGET_PRICE_UNAVAILABLE
|
||
rows = await _session_target_prices(db_engine, res.qt_id) if res.qt_id else []
|
||
assert rows == []
|
||
|
||
|
||
# ===== 헬퍼 (위 테스트들이 쓰는 도우미) =====
|
||
def _service():
|
||
return QuotationService(QuotationCRUD())
|
||
|
||
|
||
async def _seed_item(engine, company_id, *, internet_lowest):
|
||
"""상품 1건 시드(인터넷최저가만). category_type·internet_lowest_price_yn 은 NOT NULL —
|
||
ORM default 는 raw INSERT 에 안 먹으므로 명시한다(conftest companies.status 와 같은 이유)."""
|
||
item_id = uuid.uuid4()
|
||
async with engine.begin() as conn:
|
||
await conn.execute(
|
||
text(
|
||
"INSERT INTO items "
|
||
"(item_id, company_id, user_id, name, category_type, "
|
||
" internet_lowest_price_yn, internet_lowest_price) VALUES "
|
||
"(:item_id, :company_id, :user_id, '상품', 1, false, :ilp)"
|
||
),
|
||
{"item_id": item_id, "company_id": uuid.UUID(company_id),
|
||
"user_id": uuid.uuid4(), "ilp": internet_lowest},
|
||
)
|
||
return item_id
|
||
|
||
|
||
async def _session_target_prices(engine, qt_id):
|
||
"""생성된 견적의 (item_id -> target_price) 매핑."""
|
||
async with engine.begin() as conn:
|
||
rows = (await conn.execute(
|
||
text("SELECT item_id, target_price FROM sessions WHERE quotation_id = :qt"),
|
||
{"qt": qt_id},
|
||
)).all()
|
||
return rows
|