o2o-negosium-original/negodata/backend/tests/test_quotation_create.py

155 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""견적 생성 — 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 PriceGateAction, QuotationType
from crud.quotation_crud import QuotationCRUD
from router.v1.quotation.protocol import Req_CreateQuotation
from services.quotation 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 == []
async def test_auction_forces_lowest_price_award(db_engine, company_id):
"""검증: 1:N 경매(NEW_QUOTE)에 낙찰 기준(OPEN/OPEN)을 실어 생성 요청.
기대결과: 경매는 무조건 최저가 낙찰 — 견적 행에 mid=over=AWARD 로 강제 저장(요청값 무시)."""
item = await _seed_item(db_engine, company_id, internet_lowest=100_000)
req = Req_CreateQuotation(
qt_setting_id=uuid.uuid4(),
name="경매정책강제",
type=QuotationType.NEW_QUOTE.value,
end_time=FUTURE,
item_ids=[item],
supplier_ids=[uuid.uuid4(), uuid.uuid4()],
mid_action=PriceGateAction.OPEN.value, # 경매엔 의미 없음 — 서버가 덮어야 함
over_action=PriceGateAction.OPEN.value,
)
res = await _service().create_quotation(str(uuid.uuid4()), req)
assert res.result.success is True
mid, over = await _quotation_policy(db_engine, res.qt_id)
assert mid == PriceGateAction.AWARD.value, f"경매 mid_action 은 AWARD 강제여야 함, 실제 {mid}"
assert over == PriceGateAction.AWARD.value, f"경매 over_action 은 AWARD 강제여야 함, 실제 {over}"
async def test_nego_persists_award_criterion(db_engine, company_id):
"""검증: 1:1 협상(NEW_NEGO)에 낙찰 기준(mid=AWARD/over=OPEN=목표까지 낙찰)을 실어 생성.
기대결과: 요청값이 견적 행에 그대로 박제(협상은 사용자가 낙찰 기준을 정한다. 목표초과=개찰)."""
item = await _seed_item(db_engine, company_id, internet_lowest=100_000)
req = Req_CreateQuotation(
qt_setting_id=uuid.uuid4(),
name="협상정책박제",
type=QuotationType.NEW_NEGO.value,
end_time=FUTURE,
item_ids=[item],
supplier_ids=[uuid.uuid4()],
mid_action=PriceGateAction.AWARD.value,
over_action=PriceGateAction.OPEN.value,
)
res = await _service().create_quotation(str(uuid.uuid4()), req)
assert res.result.success is True
mid, over = await _quotation_policy(db_engine, res.qt_id)
assert (mid, over) == (
PriceGateAction.AWARD.value, PriceGateAction.OPEN.value,
), f"협상 낙찰 기준이 그대로 저장돼야 함, 실제 {(mid, over)}"
# ===== 헬퍼 (위 테스트들이 쓰는 도우미) =====
def _service():
return QuotationService(QuotationCRUD())
async def _quotation_policy(engine, qt_id):
"""생성된 견적의 낙찰 기준 (mid_action, over_action)."""
async with engine.begin() as conn:
return (await conn.execute(
text("SELECT mid_action, over_action FROM quotations WHERE qt_id = :qt"),
{"qt": qt_id},
)).one()
async def _seed_item(engine, company_id, *, internet_lowest, price=100_000):
"""상품 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, "
" price, internet_lowest_price_yn, internet_lowest_price) VALUES "
"(:item_id, :company_id, :user_id, '상품', 1, :price, false, :ilp)"
),
{"item_id": item_id, "company_id": uuid.UUID(company_id),
"user_id": uuid.uuid4(), "price": price, "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