165 lines
7.3 KiB
Python
165 lines
7.3 KiB
Python
"""견적 마감(close_and_decide) 테스트 — 마감하면 상황별로 결과가 맞게 판정되고, 그 결과가 작성자에게 알림으로 남는지 확인.
|
|
|
|
마감 결과는 낙찰(AWARDED) 또는 개찰(OPENED, 낙찰자 미정 마감). 개찰은 결렬(유찰)이 아니며 자동 재협상/재생성도 없다.
|
|
각 경우에 (1) 판정이 맞고 (2) 작성자 알림함에 알맞은 알림 1건이 남는지 본다:
|
|
· 단독 최저가(기준 통과) → 낙찰 (SUCCESS)
|
|
· 협상 거부 → 개찰 (알림 FAILURE, reason=rejected — 프론트에서 '개찰'로 표기)
|
|
· 동가 → 개찰 (알림 FAILURE, reason=equal)
|
|
· 전원 미응찰 → 개찰 (알림 FAILURE, reason=no_show)
|
|
|
|
공급사의 협상 결과(협상완료/거부/입찰가)는 협상 화면에서만 생기는 값이라 API 로 못 만든다 → SQL 로 직접 넣는다.
|
|
마감 판정 로직 자체를 더 깊게 파는 건 test_close_and_decide_fixes.
|
|
"""
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
import pytest_asyncio
|
|
from sqlalchemy import text
|
|
|
|
from common.enums import CloseOutcome, NotificationType, QuotationStatus, QuotationType, SessionStatus
|
|
from crud.quotation_crud import QuotationCRUD
|
|
from services.quotation import QuotationService
|
|
|
|
PAST = datetime(2020, 1, 1)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def clean(db_engine):
|
|
"""conftest 는 notifications 를 비우지 않는다 → 알림 단언이 다른 테스트에 안 흔들리게 여기서 함께 비운다."""
|
|
async with db_engine.begin() as conn:
|
|
await conn.execute(text("TRUNCATE TABLE sessions, quotations, notifications RESTART IDENTITY CASCADE"))
|
|
return db_engine
|
|
|
|
|
|
async def test_award_notifies_success(clean):
|
|
"""검증: 협상완료 세션 2건(입찰 100·200) — 단독 최저가로 마감(기본 낙찰 기준=최저가 낙찰).
|
|
기대결과: 판정 = 낙찰(AWARDED) + 알림 SUCCESS(winner_price=100=최저가, ref_qt_id=그 견적)."""
|
|
engine = clean
|
|
user_id = uuid.uuid4()
|
|
winner = uuid.uuid4()
|
|
qt = await _seed_quotation(engine, user_id=user_id, number="N-AWARD")
|
|
await _add_session(engine, qt, status=SessionStatus.DONE.value, bid_price=100, supplier_id=winner)
|
|
await _add_session(engine, qt, status=SessionStatus.DONE.value, bid_price=200)
|
|
|
|
outcome = await _service().close_and_decide(qt)
|
|
|
|
assert outcome == CloseOutcome.AWARDED
|
|
notis = await _notifications(engine, user_id)
|
|
assert len(notis) == 1
|
|
type_, data, ref = notis[0]
|
|
assert type_ == NotificationType.SUCCESS.value
|
|
assert data["winner_price"] == 100
|
|
assert str(ref) == str(qt)
|
|
|
|
|
|
async def test_rejected_notifies_open(clean):
|
|
"""검증: 협상거부 세션만 있는 상태로 마감.
|
|
기대결과: 판정 = 개찰(OPENED) + 알림(reason=rejected). 결렬 아님·자동 재생성 없음."""
|
|
engine = clean
|
|
user_id = uuid.uuid4()
|
|
qt = await _seed_quotation(engine, user_id=user_id, number="N-REJECT")
|
|
await _add_session(engine, qt, status=SessionStatus.REJECTED.value)
|
|
|
|
outcome = await _service().close_and_decide(qt)
|
|
|
|
assert outcome == CloseOutcome.OPENED
|
|
notis = await _notifications(engine, user_id)
|
|
assert len(notis) == 1
|
|
type_, data, ref = notis[0]
|
|
assert type_ == NotificationType.FAILURE.value
|
|
assert data["reason"] == "rejected"
|
|
assert str(ref) == str(qt)
|
|
|
|
|
|
async def test_equal_bid_opens(clean):
|
|
"""검증: 협상완료 세션 2건이 '동가'(둘 다 100).
|
|
기대결과: 판정 = 개찰(OPENED, 낙찰자 미정) + 알림(reason=equal). 자동 재입찰 없음."""
|
|
engine = clean
|
|
user_id = uuid.uuid4()
|
|
qt = await _seed_quotation(engine, user_id=user_id, number="N-EQUAL")
|
|
await _add_session(engine, qt, status=SessionStatus.DONE.value, bid_price=100)
|
|
await _add_session(engine, qt, status=SessionStatus.DONE.value, bid_price=100)
|
|
|
|
outcome = await _service().close_and_decide(qt)
|
|
|
|
assert outcome == CloseOutcome.OPENED
|
|
notis = await _notifications(engine, user_id)
|
|
assert len(notis) == 1
|
|
type_, data, _ = notis[0]
|
|
assert type_ == NotificationType.FAILURE.value
|
|
assert data["reason"] == "equal"
|
|
|
|
|
|
async def test_no_show_opens(clean):
|
|
"""검증: 전원 미응찰(미시작 세션만).
|
|
기대결과: 판정 = 개찰(OPENED) + 알림(reason=no_show). 자동 재소집 없음."""
|
|
engine = clean
|
|
user_id = uuid.uuid4()
|
|
qt = await _seed_quotation(engine, user_id=user_id, number="N-NOSHOW")
|
|
await _add_session(engine, qt, status=SessionStatus.CREATED.value)
|
|
await _add_session(engine, qt, status=SessionStatus.CREATED.value)
|
|
|
|
outcome = await _service().close_and_decide(qt)
|
|
|
|
assert outcome == CloseOutcome.OPENED
|
|
notis = await _notifications(engine, user_id)
|
|
assert len(notis) == 1
|
|
type_, data, _ = notis[0]
|
|
assert type_ == NotificationType.FAILURE.value
|
|
assert data["reason"] == "no_show"
|
|
|
|
|
|
# ===== 헬퍼 (세션 입찰값을 SQL 로 직접 세팅) =====
|
|
async def _seed_quotation(engine, *, user_id, number, round_=1, status=QuotationStatus.IN_PROGRESS.value):
|
|
"""견적 1건 시드(작성자=user_id). 낙찰 기준 mid/over 는 서버 기본(AWARD)=최저가 낙찰."""
|
|
qt_id = uuid.uuid4()
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO quotations "
|
|
"(qt_id, user_id, qt_setting_id, version_id, name, number, type, status, "
|
|
" round, iteration, start_time, end_time, deleted) VALUES "
|
|
"(:qt_id, :user_id, :qt_setting_id, :version_id, '견적A', :number, :type, :status, "
|
|
" :round, 0, :start_time, :end_time, false)"
|
|
),
|
|
{
|
|
"qt_id": qt_id, "user_id": user_id, "qt_setting_id": uuid.uuid4(),
|
|
"version_id": uuid.uuid4(), "number": number,
|
|
"type": QuotationType.REQUOTE.value, "status": status, "round": round_,
|
|
"start_time": PAST, "end_time": PAST,
|
|
},
|
|
)
|
|
return qt_id
|
|
|
|
|
|
async def _add_session(engine, qt_id, *, status, bid_price=None, supplier_id=None):
|
|
"""세션 1건 시드(공급사 협상 1건). status/bid_price 로 협상완료·거부·입찰가를 만든다."""
|
|
async with engine.begin() as conn:
|
|
await conn.execute(
|
|
text(
|
|
"INSERT INTO sessions "
|
|
"(session_id, quotation_id, item_id, supplier_id, qt_number, qt_round, qt_type, "
|
|
" target_price, status, bid_price, end_time) VALUES "
|
|
"(:session_id, :quotation_id, :item_id, :supplier_id, 'Q', 1, :qt_type, "
|
|
" 0, :status, :bid_price, :end_time)"
|
|
),
|
|
{
|
|
"session_id": uuid.uuid4(), "quotation_id": qt_id, "item_id": uuid.uuid4(),
|
|
"supplier_id": supplier_id or uuid.uuid4(), "qt_type": QuotationType.REQUOTE.value,
|
|
"status": status, "bid_price": bid_price, "end_time": PAST,
|
|
},
|
|
)
|
|
|
|
|
|
async def _notifications(engine, user_id):
|
|
"""user_id(작성자) 인박스 알림 (type, data, ref_qt_id) — 생성순."""
|
|
async with engine.begin() as conn:
|
|
return (await conn.execute(
|
|
text("SELECT type, data, ref_qt_id FROM notifications WHERE user_id = :uid ORDER BY created_at"),
|
|
{"uid": user_id},
|
|
)).all()
|
|
|
|
|
|
def _service():
|
|
return QuotationService(QuotationCRUD())
|