134 lines
5.0 KiB
Python
134 lines
5.0 KiB
Python
"""Equivalent date formats preserve CRM tier identity; isolated SQLite only."""
|
|
from datetime import date, datetime
|
|
|
|
import pymysql
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.crud.crm import contract_product as cp
|
|
from app.models.crm_models import CrmContractProduct, CrmContractProductPriceTier
|
|
from app.models.pl_models import PlPlan, PlPlanLoadProduct, PlPlanDeliveryProduct
|
|
from app.schemas.crm.contract import ContractUpdateBody
|
|
|
|
|
|
@pytest.fixture
|
|
def db(monkeypatch):
|
|
def forbidden(*args, **kwargs):
|
|
raise AssertionError('Business MySQL connections are forbidden')
|
|
monkeypatch.setattr(pymysql, 'connect', forbidden)
|
|
engine = create_engine('sqlite://')
|
|
for model in (CrmContractProduct, CrmContractProductPriceTier, PlPlan, PlPlanLoadProduct, PlPlanDeliveryProduct):
|
|
model.__table__.create(engine)
|
|
with Session(engine, autoflush=False) as session:
|
|
yield session
|
|
engine.dispose()
|
|
|
|
|
|
def seed(db):
|
|
parent = CrmContractProduct(id='line', tenant_no='t', contract_id='contract', product_id='sku',
|
|
product_name='Product', unit='吨', package_spec='桶', quantity=1, price=10,
|
|
create_time=datetime(2020, 1, 1), create_by='creator')
|
|
db.add(parent)
|
|
db.flush()
|
|
return parent
|
|
|
|
|
|
def tier(db, row_id='tier', **extra):
|
|
values = dict(id=row_id, tenant_no='t', contract_id='contract', contract_product_id='line',
|
|
start_date=date(2026, 1, 1), end_date=date(2026, 2, 1), price=10, remark='keep',
|
|
letter_oss_ids='["letter"]', create_time=datetime(2020, 1, 1), create_by='creator')
|
|
values.update(extra)
|
|
row = CrmContractProductPriceTier(**values)
|
|
db.add(row)
|
|
db.flush()
|
|
return row
|
|
|
|
|
|
def tier_body(**kwargs):
|
|
values = dict(startDate='2026-01-01', endDate='2026-02-01', price=20)
|
|
values.update(kwargs)
|
|
return values
|
|
|
|
|
|
def save(db, tiers):
|
|
request = ContractUpdateBody(id='contract', customerId='customer', contractType='sale',
|
|
products=[{'id': 'line', 'priceTiers': tiers}])
|
|
payload = request.model_dump(by_alias=True, exclude_unset=True)
|
|
cp.sync_contract_products(db, tenant_no='t', contract_id='contract', products=payload['products'], user_id='editor')
|
|
db.flush()
|
|
|
|
|
|
@pytest.mark.parametrize('start,end', [
|
|
('2026-01-01 00:00:00', '2026-02-01 17:05:09'),
|
|
(' 2026-01-01 ', ' 2026-02-01 '),
|
|
('2026-1-1', '2026-2-1'),
|
|
('2026-01-01', '2026-02-01'),
|
|
])
|
|
def test_missing_tier_id_uses_canonical_day_and_keeps_metadata(db, start, end):
|
|
parent = seed(db)
|
|
old = tier(db)
|
|
for price in (20, 30):
|
|
save(db, [tier_body(startDate=start, endDate=end, price=price)])
|
|
assert db.query(CrmContractProductPriceTier).count() == 1
|
|
assert old.del_flag == 0 and old.price == 30 and old.remark == 'keep'
|
|
assert old.letter_oss_ids == '["letter"]'
|
|
assert old.create_by == 'creator' and old.create_time == datetime(2020, 1, 1)
|
|
assert parent.id == 'line' and parent.del_flag == 0 and parent.unit == '吨'
|
|
|
|
|
|
@pytest.mark.parametrize('start', ['bad', '', None, '2026-02-30'])
|
|
def test_bad_anonymous_dates_rejected_before_any_changes(db, start):
|
|
seed(db)
|
|
old = tier(db)
|
|
with pytest.raises(ValueError):
|
|
save(db, [tier_body(id='tier', price=99), tier_body(startDate=start)])
|
|
assert not db.new and not db.dirty and old.price == 10
|
|
|
|
|
|
def test_canonical_duplicate_old_rows_are_ambiguous(db):
|
|
seed(db)
|
|
tier(db)
|
|
tier(db, 'duplicate')
|
|
with pytest.raises(ValueError):
|
|
save(db, [tier_body(startDate='2026-01-01 00:00:00')])
|
|
assert not db.new and not db.dirty
|
|
|
|
|
|
def test_two_anonymous_rows_cannot_claim_same_existing_interval(db):
|
|
seed(db)
|
|
tier(db)
|
|
with pytest.raises(ValueError):
|
|
save(db, [tier_body(), tier_body(startDate='2026-01-01 00:00:00')])
|
|
assert not db.new and not db.dirty
|
|
|
|
|
|
@pytest.mark.parametrize('scope', [{'tenant_no': 'other'}, {'contract_id': 'other'},
|
|
{'contract_product_id': 'other'}, {'del_flag': 1}])
|
|
def test_explicit_tier_id_must_be_active_and_in_scope(db, scope):
|
|
seed(db)
|
|
tier(db, **scope)
|
|
with pytest.raises(ValueError):
|
|
save(db, [tier_body(id='tier')])
|
|
assert not db.new and not db.dirty
|
|
|
|
|
|
def test_explicit_partial_tier_patch_and_collection_omission(db):
|
|
seed(db)
|
|
old = tier(db)
|
|
save(db, [{'id': 'tier', 'price': 25}])
|
|
cp.sync_contract_products(db, tenant_no='t', contract_id='contract', products=[{'id': 'line', 'quantity': 2}])
|
|
db.flush()
|
|
assert old.del_flag == 0 and old.price == 25 and old.start_date == date(2026, 1, 1)
|
|
save(db, [])
|
|
assert old.del_flag == 1
|
|
|
|
|
|
def test_genuinely_new_interval_is_added_and_removed_interval_soft_deleted(db):
|
|
seed(db)
|
|
old = tier(db)
|
|
save(db, [tier_body(startDate='2026-03-01 00:00:00', endDate='2026-04-01 00:00:00')])
|
|
active = db.query(CrmContractProductPriceTier).filter_by(del_flag=0).one()
|
|
assert old.del_flag == 1 and active.id != old.id
|
|
assert active.start_date == date(2026, 3, 1) and active.end_date == date(2026, 4, 1)
|