"""Product metadata saves use stable identities; SQLite only.""" from datetime import datetime import pytest from sqlalchemy import create_engine from sqlalchemy.orm import Session from app.crud.crm import product as crud from app.models.crm_models import CrmProduct, CrmProductAlias, CrmProductBrand from app.models.pl_models import PlInventory, PlPlan, PlPlanProduct @pytest.fixture def db(): engine = create_engine('sqlite://') for model in (CrmProduct, CrmProductAlias, CrmProductBrand, PlInventory, PlPlan, PlPlanProduct): model.__table__.create(engine) with Session(engine) as session: yield session def seed(db, kind, identity='old', name='Original', **kwargs): model = CrmProductBrand if kind == 'brands' else CrmProductAlias attr = 'brand_name' if kind == 'brands' else 'alias_name' values = dict(id=identity, tenant_no='t', product_id='p', del_flag=0, create_by='creator', create_time=datetime(2020, 1, 1), **{attr: name}) values.update(kwargs) row = model(**values) db.add(row) db.flush() return row def save(db, kind, values): getattr(crud, '_replace_product_' + kind)(db, product_id='p', tenant_no='t', user_id='editor', **{kind: values}) db.flush() def item(kind, name='Original', **kwargs): return {'brandName' if kind == 'brands' else 'aliasName': name, **kwargs} @pytest.mark.parametrize('kind', ['brands', 'aliases']) def test_legacy_unique_name_reuses_identity(db, kind): old = seed(db, kind) save(db, kind, [item(kind)]) assert old.del_flag == 0 assert db.query(type(old)).count() == 1 assert old.create_by == 'creator' and old.create_time == datetime(2020, 1, 1) @pytest.mark.parametrize('kind', ['brands', 'aliases']) @pytest.mark.parametrize('bad', ['missing', 'foreign_tenant', 'foreign_product', 'deleted', 'duplicate']) def test_bad_ids_rejected_before_mutation(db, kind, bad): old = seed(db, kind) foreign = {'foreign_tenant': {'tenant_no': 'other'}, 'foreign_product': {'product_id': 'other'}, 'deleted': {'del_flag': 1}} if bad in foreign: seed(db, kind, 'bad', **foreign[bad]) values = [item(kind, 'Changed', id='old'), item(kind, 'Other', id='old' if bad == 'duplicate' else 'bad')] with pytest.raises(ValueError): save(db, kind, values) assert old.del_flag == 0 and not db.dirty and not db.new @pytest.mark.parametrize('kind', ['brands', 'aliases']) def test_ambiguous_legacy_name_is_rejected(db, kind): old = seed(db, kind) seed(db, kind, 'second') with pytest.raises(ValueError): save(db, kind, [item(kind)]) assert old.del_flag == 0 and not db.dirty @pytest.mark.parametrize('kind', ['brands', 'aliases']) def test_edit_add_remove_and_omission(db, kind): old = seed(db, kind) removed = seed(db, kind, 'removed', name='Remove') save(db, kind, None) assert old.del_flag == removed.del_flag == 0 save(db, kind, [item(kind, 'Changed', id='old'), item(kind, 'New')]) assert old.del_flag == 0 and removed.del_flag == 1 rows = db.query(type(old)).filter_by(del_flag=0).all() assert len(rows) == 2 and len({row.id for row in rows}) == 2 save(db, kind, []) assert db.query(type(old)).filter_by(del_flag=0).count() == 0 def test_validate_both_collections_before_header_or_other_collection_mutation(db): parent = CrmProduct(id='p', tenant_no='t', product_no='P', product_name='Original', del_flag=0) db.add(parent) brand = seed(db, 'brands') seed(db, 'aliases', 'alias') with pytest.raises(ValueError): crud.update_product(db, 'p', product_name='Changed', brands=[], aliases=[item('aliases', id='foreign')]) assert parent.product_name == 'Original' and brand.del_flag == 0 assert not db.dirty and not db.new def test_existing_brand_omitted_status_preserves_it(db): old = seed(db, 'brands', status='off') save(db, 'brands', [item('brands', 'Renamed', id='old')]) assert old.status == 'off' def test_brand_removal_guard_runs_before_any_mutation(db): from fastapi import HTTPException old = seed(db, 'brands') guarded = seed(db, 'brands', 'guarded', name='Guarded') db.add(PlInventory(id='inventory', tenant_no='t', product_id='p', brand_id='guarded', product_no='P', product_name='Product', stock_qty=1, del_flag=0)) db.flush() with pytest.raises(HTTPException): save(db, 'brands', []) assert old.del_flag == guarded.del_flag == 0 and not db.dirty @pytest.mark.parametrize('kind', ['brands', 'aliases']) def test_create_rejects_source_ids_before_adding_parent(db, kind): with pytest.raises(ValueError): crud.create_product(db, tenant_no='t', product_no='P', product_name='New', barcode='123', **{kind: [item(kind, id='source')]}) assert db.query(CrmProduct).count() == 0 and not db.new @pytest.mark.parametrize('kind', ['brands', 'aliases']) def test_explicit_same_name_and_new_row_have_distinct_identity(db, kind): old = seed(db, kind) save(db, kind, [item(kind, id='old'), item(kind)]) assert old.del_flag == 0 assert db.query(type(old)).filter_by(del_flag=0).count() == 2 @pytest.mark.parametrize('kind', ['brands', 'aliases']) def test_repeated_anonymous_names_rejected_before_mutation(db, kind): old = seed(db, kind) with pytest.raises(ValueError): save(db, kind, [item(kind), item(kind)]) assert old.del_flag == 0 and not db.dirty def test_omitted_product_images_stay_and_empty_string_clears(db): parent = CrmProduct( id='p', tenant_no='t', product_no='P', product_name='甲醇', images='https://a/1.jpg,https://a/2.jpg', del_flag=0, ) db.add(parent) db.commit() crud.update_product(db, 'p', remark='只改备注') assert parent.images == 'https://a/1.jpg,https://a/2.jpg' crud.update_product(db, 'p', images='') assert parent.images == ''