"""Integration tests for contract term transfer endpoints (PORT-55). Covers: POST /account//contract-terms/attachments/bulk-add DELETE /account//contract-terms/attachments/bulk """ import json import pytest from abacus_common_logic.connectors.database import db from abacus_contract.tests.integration.conftest import ows_abacus_contract_api_client from abacus_contract.tests.integration.consts.headers import ADMIN_HEADERS from abacus_contract.tests.integration.utils.generic_helper import ( create_account_and_contract, ) # A condition payload that satisfies the required term_rate + priority fields. _CONDITION = {'conditions': {}, 'term_rate': 80.0, 'priority': 1} def _seed_contract_term(contract_id, term_type, attachments): """Insert a contract_term row and return its contract_term_id.""" attachments_json = json.dumps(attachments) result = db.engine.execute( f""" INSERT INTO contract_term ( contract_id, term_type, attachments, created_by, created_at, last_modified_by, last_modified ) VALUES ( {contract_id}, '{term_type}', '{attachments_json}', 'integration_tests', NOW(), 'integration_tests', NOW() ) """ ) return result.lastrowid def _fetch_term_attachments(contract_term_id): """Return the attachments list for a contract_term row.""" row = db.engine.execute( f'SELECT attachments FROM contract_term WHERE contract_term_id = {contract_term_id}' ).fetchone() return json.loads(row[0]) if row and row[0] else [] def _fetch_term_conditions(contract_term_id): """Return all non-deleted condition rows for a contract_term.""" rows = db.engine.execute( f""" SELECT contract_term_condition_id, term_rate, commission, priority FROM contract_term_condition WHERE contract_term_id = {contract_term_id} AND deleted_at IS NULL """ ).fetchall() return rows def _fetch_term_by_type(contract_id, term_type): """Return the first active contract_term row for a given contract and type.""" return db.engine.execute( f""" SELECT contract_term_id, attachments FROM contract_term WHERE contract_id = {contract_id} AND term_type = '{term_type}' AND deleted_at IS NULL LIMIT 1 """ ).fetchone() # --------------------------------------------------------------------------- # POST bulk-add — merging into existing terms # --------------------------------------------------------------------------- @pytest.mark.jira('PORT-55') def test_bulk_add_merges_upcs_into_existing_product_term(admin_headers): """POST bulk-add merges incoming UPCs into an existing product term, deduplicating.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() _seed_contract_term(contract_id, 'product', ['111', '222']) res = client.post_bulk_add_contract_term_attachments( account_id, { 'upcs': ['222', '333'], 'isrcs': [], 'contract_id': contract_id, 'conditions': [], }, ) assert res.status_code == 200 body = res.json() assert body['total_modified'] == 1 assert len(body['updated_terms']) == 1 assert body['created_terms'] == [] assert body['updated_terms'][0]['term_type'] == 'product' assert set(body['updated_terms'][0]['added_values']) == {'222', '333'} db_attachments = _fetch_term_attachments(body['updated_terms'][0]['term_id']) assert set(db_attachments) == {'111', '222', '333'} @pytest.mark.jira('PORT-55') def test_bulk_add_merges_isrcs_into_existing_track_term(admin_headers): """POST bulk-add merges incoming ISRCs into an existing track term, deduplicating.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() _seed_contract_term(contract_id, 'track', ['ISRC1']) res = client.post_bulk_add_contract_term_attachments( account_id, { 'upcs': [], 'isrcs': ['ISRC1', 'ISRC2'], 'contract_id': contract_id, 'conditions': [], }, ) assert res.status_code == 200 body = res.json() assert body['total_modified'] == 1 assert body['updated_terms'][0]['term_type'] == 'track' db_attachments = _fetch_term_attachments(body['updated_terms'][0]['term_id']) assert set(db_attachments) == {'ISRC1', 'ISRC2'} @pytest.mark.jira('PORT-55') def test_bulk_add_both_upcs_and_isrcs_updates_both_terms(admin_headers): """POST bulk-add with both upcs and isrcs updates product and track terms in one call.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() _seed_contract_term(contract_id, 'product', ['111']) _seed_contract_term(contract_id, 'track', ['ISRC1']) res = client.post_bulk_add_contract_term_attachments( account_id, { 'upcs': ['222'], 'isrcs': ['ISRC2'], 'contract_id': contract_id, 'conditions': [], }, ) assert res.status_code == 200 body = res.json() assert body['total_modified'] == 2 assert len(body['updated_terms']) == 2 assert body['created_terms'] == [] term_types = {t['term_type'] for t in body['updated_terms']} assert term_types == {'product', 'track'} # --------------------------------------------------------------------------- # POST bulk-add — creating new terms # --------------------------------------------------------------------------- @pytest.mark.jira('PORT-55') def test_bulk_add_creates_new_product_term_with_conditions(admin_headers): """POST bulk-add creates a new product term and its condition rows when none exists.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() res = client.post_bulk_add_contract_term_attachments( account_id, { 'upcs': ['111'], 'isrcs': [], 'contract_id': contract_id, 'conditions': [_CONDITION], }, ) assert res.status_code == 200 body = res.json() assert body['total_modified'] == 1 assert body['updated_terms'] == [] assert len(body['created_terms']) == 1 created = body['created_terms'][0] assert created['term_type'] == 'product' assert created['added_values'] == ['111'] db_conditions = _fetch_term_conditions(created['term_id']) assert len(db_conditions) == 1 assert float(db_conditions[0]['term_rate']) == pytest.approx(80.0) assert float(db_conditions[0]['commission']) == pytest.approx(20.0) assert db_conditions[0]['priority'] == 1 @pytest.mark.jira('PORT-55') def test_bulk_add_creates_new_track_term_with_conditions(admin_headers): """POST bulk-add creates a new track term and its condition rows when none exists.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() res = client.post_bulk_add_contract_term_attachments( account_id, { 'upcs': [], 'isrcs': ['ISRC1'], 'contract_id': contract_id, 'conditions': [_CONDITION], }, ) assert res.status_code == 200 body = res.json() assert body['created_terms'][0]['term_type'] == 'track' db_conditions = _fetch_term_conditions(body['created_terms'][0]['term_id']) assert len(db_conditions) == 1 @pytest.mark.jira('PORT-55') def test_bulk_add_creates_multiple_condition_rows(admin_headers): """POST bulk-add creates one ContractTermCondition row per entry in the conditions list.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() conditions = [ {'conditions': {'territory': 'US'}, 'term_rate': 75.0, 'priority': 1}, {'conditions': {'territory': 'EU'}, 'term_rate': 70.0, 'priority': 2}, ] res = client.post_bulk_add_contract_term_attachments( account_id, { 'upcs': ['111'], 'isrcs': [], 'contract_id': contract_id, 'conditions': conditions, }, ) assert res.status_code == 200 term_id = res.json()['created_terms'][0]['term_id'] db_conditions = _fetch_term_conditions(term_id) assert len(db_conditions) == 2 # --------------------------------------------------------------------------- # POST bulk-add — idempotency # --------------------------------------------------------------------------- @pytest.mark.jira('PORT-55') def test_bulk_add_is_idempotent_for_duplicate_upcs(admin_headers): """Calling bulk-add twice with the same UPCs does not duplicate attachments.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() _seed_contract_term(contract_id, 'product', ['111']) payload = { 'upcs': ['111'], 'isrcs': [], 'contract_id': contract_id, 'conditions': [], } res1 = client.post_bulk_add_contract_term_attachments(account_id, payload) res2 = client.post_bulk_add_contract_term_attachments(account_id, payload) assert res1.status_code == 200 assert res2.status_code == 200 term_row = _fetch_term_by_type(contract_id, 'product') db_attachments = json.loads(term_row['attachments']) assert db_attachments.count('111') == 1 # --------------------------------------------------------------------------- # POST bulk-add — response shape # --------------------------------------------------------------------------- @pytest.mark.jira('PORT-55') def test_bulk_add_response_shape_on_update(admin_headers): """Response for an update has the expected top-level keys and per-term fields.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() _seed_contract_term(contract_id, 'product', ['111']) res = client.post_bulk_add_contract_term_attachments( account_id, {'upcs': ['222'], 'isrcs': [], 'contract_id': contract_id, 'conditions': []}, ) assert res.status_code == 200 body = res.json() assert set(body.keys()) >= {'updated_terms', 'created_terms', 'total_modified'} term = body['updated_terms'][0] assert set(term.keys()) >= { 'contract_id', 'term_id', 'term_type', 'added_values', 'attachments', } @pytest.mark.jira('PORT-55') def test_bulk_add_response_shape_on_create(admin_headers): """Response for a create has the expected top-level keys and per-term fields.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() res = client.post_bulk_add_contract_term_attachments( account_id, { 'upcs': ['111'], 'isrcs': [], 'contract_id': contract_id, 'conditions': [_CONDITION], }, ) assert res.status_code == 200 body = res.json() assert body['updated_terms'] == [] term = body['created_terms'][0] assert set(term.keys()) >= { 'contract_id', 'term_id', 'term_type', 'added_values', 'attachments', } # --------------------------------------------------------------------------- # POST bulk-add — validation errors # --------------------------------------------------------------------------- @pytest.mark.jira('PORT-55') def test_bulk_add_returns_400_when_both_lists_empty(admin_headers): """POST bulk-add with empty upcs and isrcs returns a 400 validation error.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() res = client.post_bulk_add_contract_term_attachments( account_id, {'upcs': [], 'isrcs': [], 'contract_id': contract_id, 'conditions': []}, ) assert res.status_code == 400 @pytest.mark.jira('PORT-55') def test_bulk_add_returns_400_when_contract_id_missing(admin_headers): """POST bulk-add without contract_id returns a 400 validation error.""" client = ows_abacus_contract_api_client(admin_headers) _, _, account_id = create_account_and_contract() res = client.post_bulk_add_contract_term_attachments( account_id, {'upcs': ['111'], 'isrcs': [], 'conditions': []}, ) assert res.status_code == 400 # --------------------------------------------------------------------------- # DELETE bulk-remove — happy path # --------------------------------------------------------------------------- @pytest.mark.jira('PORT-55') def test_bulk_remove_removes_upcs_from_product_term(admin_headers): """DELETE bulk removes specified UPCs from a product term's attachments.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() term_id = _seed_contract_term(contract_id, 'product', ['111', '222', '333']) res = client.delete_bulk_remove_contract_term_attachments( account_id, {'upcs': ['222', '333'], 'isrcs': []}, ) assert res.status_code == 200 body = res.json() assert body['total_removed'] == 1 assert body['updated_terms'][0]['removed_attachments'] == ['222', '333'] db_attachments = _fetch_term_attachments(term_id) assert db_attachments == ['111'] @pytest.mark.jira('PORT-55') def test_bulk_remove_soft_deletes_term_when_all_attachments_removed(admin_headers): """DELETE bulk soft-deletes a term when removing empties its attachment list.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() _seed_contract_term(contract_id, 'product', ['111']) res = client.delete_bulk_remove_contract_term_attachments( account_id, {'upcs': ['111'], 'isrcs': []}, ) assert res.status_code == 200 body = res.json() assert body['updated_terms'][0]['soft_deleted'] is True deleted_row = db.engine.execute( f""" SELECT deleted_at FROM contract_term WHERE contract_id = {contract_id} AND term_type = 'product' LIMIT 1 """ ).fetchone() assert deleted_row is not None assert deleted_row['deleted_at'] is not None @pytest.mark.jira('PORT-55') def test_bulk_remove_is_idempotent(admin_headers): """DELETE bulk-remove is idempotent — repeating a removal does not error.""" client = ows_abacus_contract_api_client(admin_headers) contract_id, _, account_id = create_account_and_contract() _seed_contract_term(contract_id, 'product', ['111', '222']) payload = {'upcs': ['222'], 'isrcs': []} res1 = client.delete_bulk_remove_contract_term_attachments(account_id, payload) res2 = client.delete_bulk_remove_contract_term_attachments(account_id, payload) assert res1.status_code == 200 assert res2.status_code == 200 # --------------------------------------------------------------------------- # DELETE bulk-remove — validation errors # --------------------------------------------------------------------------- @pytest.mark.jira('PORT-55') def test_bulk_remove_returns_400_when_both_lists_empty(admin_headers): """DELETE bulk-remove with empty upcs and isrcs returns 400.""" client = ows_abacus_contract_api_client(admin_headers) _, _, account_id = create_account_and_contract() res = client.delete_bulk_remove_contract_term_attachments( account_id, {'upcs': [], 'isrcs': []}, ) assert res.status_code == 400