From 13625ce421520f3979a4a1b326d0fff7010ec6fc Mon Sep 17 00:00:00 2001 From: argush3 Date: Fri, 6 Mar 2026 09:20:43 -0800 Subject: [PATCH 1/5] 32736 Update /search/affiliation_mappings endpoint to support migrated businesses --- .../src/legal_api/services/search_service.py | 73 +++++--- .../tests/unit/resources/v2/test_business.py | 156 ++++++++++++++++++ 2 files changed, 208 insertions(+), 21 deletions(-) diff --git a/legal-api/src/legal_api/services/search_service.py b/legal-api/src/legal_api/services/search_service.py index 93d4e649fd..d456502a8c 100644 --- a/legal-api/src/legal_api/services/search_service.py +++ b/legal-api/src/legal_api/services/search_service.py @@ -298,16 +298,6 @@ def get_search_filtered_filings_results(business_json, @staticmethod def get_affiliation_mapping_results(identifiers): """Return affiliation mapping results for the given list of identifiers.""" - query = db.session.query( - Business._identifier.label("identifier"), # pylint: disable=protected-access - Filing - .filing_json["filing"][Filing._filing_type]["nameRequest"]["nrNumber"] # pylint: disable=protected-access - .label("nrNumber"), - RegistrationBootstrap._identifier.label("bootstrapIdentifier") # pylint: disable=protected-access - ).select_from(Filing) \ - .outerjoin(Business, Filing.business_id == Business.id) \ - .join(RegistrationBootstrap, Filing.temp_reg == RegistrationBootstrap.identifier) - temp_identifiers, nr_identifiers, business_identifiers = [], [], [] for identifier in identifiers: if identifier.startswith("T"): @@ -316,19 +306,60 @@ def get_affiliation_mapping_results(identifiers): nr_identifiers.append(identifier) else: business_identifiers.append(identifier) - conditions = [] + + result_list = [] + + # Permanent business identifiers, including migrated businesses, can exist without any + # filing/bootstrap linkage and must be resolved directly from the businesses table. if business_identifiers: - conditions.append(Business._identifier.in_(business_identifiers)) # pylint: disable=protected-access + business_rows = db.session.query( + Business._identifier.label("identifier") # pylint: disable=protected-access + ).filter( + Business._identifier.in_(business_identifiers) # pylint: disable=protected-access + ).all() + + result_list.extend([ + { + "identifier": row.identifier, + "nrNumber": None, + "bootstrapIdentifier": None + } + for row in business_rows + ]) + + draft_conditions = [] if nr_identifiers: - conditions.append(Filing - .filing_json["filing"][Filing._filing_type] # pylint: disable=protected-access - ["nameRequest"]["nrNumber"] - .astext.in_(nr_identifiers)) + draft_conditions.append( + Filing + .filing_json["filing"][Filing._filing_type] # pylint: disable=protected-access + ["nameRequest"]["nrNumber"] + .astext.in_(nr_identifiers) + ) if temp_identifiers: - conditions.append(RegistrationBootstrap._identifier.in_(identifiers)) # pylint: disable=protected-access - query = query.filter(db.or_(*conditions)) - - rows = query.all() - result_list = [dict(row) for row in rows] + draft_conditions.append( + RegistrationBootstrap._identifier.in_(temp_identifiers) # pylint: disable=protected-access + ) + + if draft_conditions: + draft_rows = db.session.query( + Business._identifier.label("identifier"), # pylint: disable=protected-access + Filing + .filing_json["filing"][Filing._filing_type]["nameRequest"]["nrNumber"] # pylint: disable=protected-access + .label("nrNumber"), + RegistrationBootstrap._identifier.label("bootstrapIdentifier") # pylint: disable=protected-access + ).select_from(Filing) \ + .outerjoin(Business, Filing.business_id == Business.id) \ + .join(RegistrationBootstrap, Filing.temp_reg == RegistrationBootstrap.identifier) \ + .filter(db.or_(*draft_conditions)) \ + .all() + + result_list.extend([ + { + "identifier": row.identifier, + "nrNumber": row.nrNumber, + "bootstrapIdentifier": row.bootstrapIdentifier + } + for row in draft_rows + ]) return result_list diff --git a/legal-api/tests/unit/resources/v2/test_business.py b/legal-api/tests/unit/resources/v2/test_business.py index cc67e6c207..f06a9bcdaa 100644 --- a/legal-api/tests/unit/resources/v2/test_business.py +++ b/legal-api/tests/unit/resources/v2/test_business.py @@ -732,6 +732,162 @@ def test_post_affiliated_businesses_invalid(session, client, jwt): assert rv.status_code == HTTPStatus.BAD_REQUEST +def _create_affiliation_mapping_draft(identifier, + filing_name='registration', + legal_type=Business.LegalTypes.SOLE_PROP.value, + nr_number=None, + legal_name='Test NR Name'): + """Create a draft filing with a temp registration for affiliation mapping tests.""" + temp_reg = RegistrationBootstrap() + temp_reg._identifier = identifier + temp_reg.save() + + json_data = copy.deepcopy(FILING_HEADER) + json_data['filing']['header']['name'] = filing_name + json_data['filing']['header']['identifier'] = identifier + del json_data['filing']['business'] + json_data['filing'][filing_name] = { + 'nameRequest': { + 'legalType': legal_type + } + } + + if nr_number: + json_data['filing'][filing_name]['nameRequest'] = { + **json_data['filing'][filing_name]['nameRequest'], + 'nrNumber': nr_number, + 'legalName': legal_name + } + + filing = factory_pending_filing(None, json_data) + filing.temp_reg = identifier + filing.save() + + +def test_post_affiliation_mappings_migrated_business_without_bootstrap(session, client, jwt): + """Assert that direct business identifiers return a mapping even without bootstrap-linked filings.""" + identifier = 'BC1328381' + factory_business_model(legal_name=identifier + 'name', + identifier=identifier, + founding_date=datetime.utcfromtimestamp(0), + last_ledger_timestamp=datetime.utcfromtimestamp(0), + last_modified=datetime.utcfromtimestamp(0), + fiscal_year_end_date=None, + tax_id=None, + dissolution_date=None, + legal_type=Business.LegalTypes.BCOMP.value) + + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': [identifier]}, + headers=create_header(jwt, [SYSTEM_ROLE])) + + assert rv.status_code == HTTPStatus.OK + assert rv.json['count'] == 1 + assert rv.json['entityDetails'] == [{ + 'identifier': identifier, + 'nrNumber': None, + 'bootstrapIdentifier': None + }] + + +def test_post_affiliation_mappings_temp_identifier(session, client, jwt): + """Assert that temp identifiers still resolve through the filing/bootstrap path.""" + identifier = 'Tb31yQIuC1' + _create_affiliation_mapping_draft(identifier=identifier, + filing_name='incorporationApplication', + legal_type=Business.LegalTypes.BCOMP.value) + + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': [identifier]}, + headers=create_header(jwt, [SYSTEM_ROLE])) + + assert rv.status_code == HTTPStatus.OK + assert rv.json['count'] == 1 + detail = rv.json['entityDetails'][0] + assert detail['bootstrapIdentifier'] == identifier + assert detail['nrNumber'] is None + + +def test_post_affiliation_mappings_nr_identifier(session, client, jwt): + """Assert that NR identifiers still resolve through the filing/bootstrap path.""" + bootstrap_identifier = 'Tb31yQIuC2' + nr_number = 'NR 1234567' + _create_affiliation_mapping_draft(identifier=bootstrap_identifier, + filing_name='registration', + legal_type=Business.LegalTypes.SOLE_PROP.value, + nr_number=nr_number, + legal_name='Test NR Name') + + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': [nr_number]}, + headers=create_header(jwt, [SYSTEM_ROLE])) + + assert rv.status_code == HTTPStatus.OK + assert rv.json['count'] == 1 + detail = rv.json['entityDetails'][0] + assert detail['nrNumber'] == nr_number + assert detail['bootstrapIdentifier'] == bootstrap_identifier + + +def test_post_affiliation_mappings_mixed_direct_and_nr_identifiers(session, client, jwt): + """Assert that direct business and draft-backed NR lookups coexist in a single request.""" + business_identifier = 'BC7654321' + nr_number = 'NR 1234568' + bootstrap_identifier = 'Tb31yQIuC3' + + factory_business_model(legal_name=business_identifier + 'name', + identifier=business_identifier, + founding_date=datetime.utcfromtimestamp(0), + last_ledger_timestamp=datetime.utcfromtimestamp(0), + last_modified=datetime.utcfromtimestamp(0), + fiscal_year_end_date=None, + tax_id=None, + dissolution_date=None, + legal_type=Business.LegalTypes.BCOMP.value) + + _create_affiliation_mapping_draft(identifier=bootstrap_identifier, + filing_name='registration', + legal_type=Business.LegalTypes.SOLE_PROP.value, + nr_number=nr_number, + legal_name='Mixed NR Name') + + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': [business_identifier, nr_number]}, + headers=create_header(jwt, [SYSTEM_ROLE])) + + assert rv.status_code == HTTPStatus.OK + assert rv.json['count'] == 2 + + business_detail = next( + detail for detail in rv.json['entityDetails'] + if detail['identifier'] == business_identifier + ) + nr_detail = next( + detail for detail in rv.json['entityDetails'] + if detail['nrNumber'] == nr_number + ) + + assert business_detail['bootstrapIdentifier'] is None + assert business_detail['nrNumber'] is None + assert nr_detail['bootstrapIdentifier'] == bootstrap_identifier + + +def test_post_affiliation_mappings_unauthorized(session, client, jwt): + """Assert that the affiliation mappings endpoint is unauthorized for non-system tokens.""" + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': ['BC1234567']}, + headers=create_header(jwt, [STAFF_ROLE])) + assert rv.status_code == HTTPStatus.UNAUTHORIZED + + +def test_post_affiliation_mappings_invalid(session, client, jwt): + """Assert that the affiliation mappings endpoint is bad request when identifiers are not given.""" + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={}, + headers=create_header(jwt, [SYSTEM_ROLE])) + assert rv.status_code == HTTPStatus.BAD_REQUEST + + def test_get_could_file(session, client, jwt, monkeypatch): """Assert that the cold file is returned.""" monkeypatch.setattr( From 8c00c1e5d5da44e611fc7c23cc4477cf9a0dc507 Mon Sep 17 00:00:00 2001 From: Rajandeep Date: Fri, 6 Mar 2026 15:35:25 -0800 Subject: [PATCH 2/5] updated business identifer logic --- .../src/legal_api/services/search_service.py | 95 +++++++++---------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/legal-api/src/legal_api/services/search_service.py b/legal-api/src/legal_api/services/search_service.py index d456502a8c..47aaa3839e 100644 --- a/legal-api/src/legal_api/services/search_service.py +++ b/legal-api/src/legal_api/services/search_service.py @@ -298,6 +298,16 @@ def get_search_filtered_filings_results(business_json, @staticmethod def get_affiliation_mapping_results(identifiers): """Return affiliation mapping results for the given list of identifiers.""" + query = db.session.query( + Business._identifier.label("identifier"), # pylint: disable=protected-access + Filing + .filing_json["filing"][Filing._filing_type]["nameRequest"]["nrNumber"] # pylint: disable=protected-access + .label("nrNumber"), + RegistrationBootstrap._identifier.label("bootstrapIdentifier") # pylint: disable=protected-access + ).select_from(Filing) \ + .outerjoin(Business, Filing.business_id == Business.id) \ + .join(RegistrationBootstrap, Filing.temp_reg == RegistrationBootstrap.identifier) + temp_identifiers, nr_identifiers, business_identifiers = [], [], [] for identifier in identifiers: if identifier.startswith("T"): @@ -306,60 +316,43 @@ def get_affiliation_mapping_results(identifiers): nr_identifiers.append(identifier) else: business_identifiers.append(identifier) + draft_conditions = [] + if business_identifiers: + draft_conditions.append(Business._identifier.in_(business_identifiers)) # pylint: disable=protected-access + if nr_identifiers: + draft_conditions.append(Filing + .filing_json["filing"][Filing._filing_type] # pylint: disable=protected-access + ["nameRequest"]["nrNumber"] + .astext.in_(nr_identifiers)) + if temp_identifiers: + draft_conditions.append(RegistrationBootstrap._identifier.in_(identifiers)) # pylint: disable=protected-access + + if not draft_conditions: + return [] + + draft_query = query.filter(db.or_(*draft_conditions)) + draft_rows = draft_query.all() result_list = [] - - # Permanent business identifiers, including migrated businesses, can exist without any - # filing/bootstrap linkage and must be resolved directly from the businesses table. - if business_identifiers: - business_rows = db.session.query( - Business._identifier.label("identifier") # pylint: disable=protected-access - ).filter( - Business._identifier.in_(business_identifiers) # pylint: disable=protected-access - ).all() - - result_list.extend([ - { + draft_identifier = set() + for row in draft_rows: + result_list.append({ + "identifier": row.identifier, + "nrNumber": row.nrNumber, + "bootstrapIdentifier": row.bootstrapIdentifier + }) + if row.identifier: + draft_identifier.add(row.identifier) + missing_identifiers = set(identifiers) - draft_identifier + if missing_identifiers: + business_query = db.session.query(Business._identifier.label("identifier")) \ + .filter(Business._identifier.in_(missing_identifiers)) # pylint: disable=protected-access + business_rows = business_query.all() + for row in business_rows: + result_list.append({ "identifier": row.identifier, "nrNumber": None, "bootstrapIdentifier": None - } - for row in business_rows - ]) - - draft_conditions = [] - if nr_identifiers: - draft_conditions.append( - Filing - .filing_json["filing"][Filing._filing_type] # pylint: disable=protected-access - ["nameRequest"]["nrNumber"] - .astext.in_(nr_identifiers) - ) - if temp_identifiers: - draft_conditions.append( - RegistrationBootstrap._identifier.in_(temp_identifiers) # pylint: disable=protected-access - ) - - if draft_conditions: - draft_rows = db.session.query( - Business._identifier.label("identifier"), # pylint: disable=protected-access - Filing - .filing_json["filing"][Filing._filing_type]["nameRequest"]["nrNumber"] # pylint: disable=protected-access - .label("nrNumber"), - RegistrationBootstrap._identifier.label("bootstrapIdentifier") # pylint: disable=protected-access - ).select_from(Filing) \ - .outerjoin(Business, Filing.business_id == Business.id) \ - .join(RegistrationBootstrap, Filing.temp_reg == RegistrationBootstrap.identifier) \ - .filter(db.or_(*draft_conditions)) \ - .all() - - result_list.extend([ - { - "identifier": row.identifier, - "nrNumber": row.nrNumber, - "bootstrapIdentifier": row.bootstrapIdentifier - } - for row in draft_rows - ]) + }) - return result_list + return result_list \ No newline at end of file From b3d1458115c9d46b2f2415c36bd985c9ee6e96f4 Mon Sep 17 00:00:00 2001 From: Rajandeep Date: Mon, 9 Mar 2026 10:29:08 -0700 Subject: [PATCH 3/5] updated --- legal-api/src/legal_api/core/filing.py | 1 + .../src/legal_api/services/search_service.py | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/legal-api/src/legal_api/core/filing.py b/legal-api/src/legal_api/core/filing.py index 082c553363..3c450e43cc 100644 --- a/legal-api/src/legal_api/core/filing.py +++ b/legal-api/src/legal_api/core/filing.py @@ -102,6 +102,7 @@ class FilingTypes(str, Enum): SPECIALRESOLUTION = "specialResolution" TRANSITION = "transition" TRANSPARENCY_REGISTER = "transparencyRegister" + TOMBSTONE = "lear_tombstone" class FilingTypesCompact(str, Enum): """Render enum for filing types with sub-types.""" diff --git a/legal-api/src/legal_api/services/search_service.py b/legal-api/src/legal_api/services/search_service.py index 47aaa3839e..bc0ab90640 100644 --- a/legal-api/src/legal_api/services/search_service.py +++ b/legal-api/src/legal_api/services/search_service.py @@ -24,6 +24,7 @@ from sqlalchemy import func from legal_api.models import Business, Filing, RegistrationBootstrap, db +from legal_api.core.filing import Filing as CoreFiling @dataclass @@ -86,7 +87,6 @@ class BusinessSearchService: # pylint: disable=too-many-public-methods EXCLUDED_FILINGS_STATUS: Final = [ Filing.Status.WITHDRAWN.value ] - @staticmethod def check_and_get_respective_values(codes): """Check if codes belong to BUSINESS_TEMP_FILINGS_CORP_CODES and return the matching ones.""" @@ -343,16 +343,18 @@ def get_affiliation_mapping_results(identifiers): }) if row.identifier: draft_identifier.add(row.identifier) - missing_identifiers = set(identifiers) - draft_identifier - if missing_identifiers: - business_query = db.session.query(Business._identifier.label("identifier")) \ - .filter(Business._identifier.in_(missing_identifiers)) # pylint: disable=protected-access - business_rows = business_query.all() - for row in business_rows: - result_list.append({ + migrated_identifiers = db.session.query( Business._identifier.label("identifier"), # pylint: disable=protected-access + Filing + ._filing_type # pylint: disable=protected-access + .label("filing_type"), + ).select_from(Filing) \ + .join(Business, Filing.business_id == Business.id) # pylint: disable=protected-access + migrated_rows = migrated_identifiers.filter(Business._identifier.in_(business_identifiers), + Filing._filing_type == CoreFiling.FilingTypes.TOMBSTONE.value).all() # pylint: disable=protected-access + for row in migrated_rows: + result_list.append({ "identifier": row.identifier, "nrNumber": None, "bootstrapIdentifier": None }) - return result_list \ No newline at end of file From 4f48b875fb5d357791a2885d8962217a89e16a86 Mon Sep 17 00:00:00 2001 From: Rajandeep Date: Mon, 9 Mar 2026 11:00:06 -0700 Subject: [PATCH 4/5] updated query filters for colin firms --- legal-api/src/legal_api/services/search_service.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/legal-api/src/legal_api/services/search_service.py b/legal-api/src/legal_api/services/search_service.py index bc0ab90640..ecb262d9e9 100644 --- a/legal-api/src/legal_api/services/search_service.py +++ b/legal-api/src/legal_api/services/search_service.py @@ -16,15 +16,15 @@ # pylint: disable=singleton-comparison ; pylint does not recognize sqlalchemy == from dataclasses import dataclass from datetime import datetime, timezone -from operator import and_ +from operator import and_, or_ from typing import Final, Optional from flask import current_app from requests import Request from sqlalchemy import func -from legal_api.models import Business, Filing, RegistrationBootstrap, db from legal_api.core.filing import Filing as CoreFiling +from legal_api.models import Business, Filing, RegistrationBootstrap, db @dataclass @@ -348,9 +348,11 @@ def get_affiliation_mapping_results(identifiers): ._filing_type # pylint: disable=protected-access .label("filing_type"), ).select_from(Filing) \ - .join(Business, Filing.business_id == Business.id) # pylint: disable=protected-access - migrated_rows = migrated_identifiers.filter(Business._identifier.in_(business_identifiers), - Filing._filing_type == CoreFiling.FilingTypes.TOMBSTONE.value).all() # pylint: disable=protected-access + .join(Business, Filing.business_id == Business.id).filter(Business._identifier.in_(business_identifiers)) + migrated_rows = migrated_identifiers.filter( + or_(Filing._filing_type == CoreFiling.FilingTypes.TOMBSTONE.value, + and_(Business.legal_type.in_(["SP", "GP"]),Business.identifier.like("FM0%"))) + ).all() for row in migrated_rows: result_list.append({ "identifier": row.identifier, From 0966a9de1ddf918e523e0debb8d3f14b2bdd9348 Mon Sep 17 00:00:00 2001 From: Rajandeep Date: Mon, 9 Mar 2026 13:03:12 -0700 Subject: [PATCH 5/5] updated test --- .../src/legal_api/services/search_service.py | 3 --- .../tests/unit/resources/v2/test_business.py | 17 ++++++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/legal-api/src/legal_api/services/search_service.py b/legal-api/src/legal_api/services/search_service.py index ecb262d9e9..1af3c4c529 100644 --- a/legal-api/src/legal_api/services/search_service.py +++ b/legal-api/src/legal_api/services/search_service.py @@ -334,15 +334,12 @@ def get_affiliation_mapping_results(identifiers): draft_rows = draft_query.all() result_list = [] - draft_identifier = set() for row in draft_rows: result_list.append({ "identifier": row.identifier, "nrNumber": row.nrNumber, "bootstrapIdentifier": row.bootstrapIdentifier }) - if row.identifier: - draft_identifier.add(row.identifier) migrated_identifiers = db.session.query( Business._identifier.label("identifier"), # pylint: disable=protected-access Filing ._filing_type # pylint: disable=protected-access diff --git a/legal-api/tests/unit/resources/v2/test_business.py b/legal-api/tests/unit/resources/v2/test_business.py index f06a9bcdaa..ee601b5ecd 100644 --- a/legal-api/tests/unit/resources/v2/test_business.py +++ b/legal-api/tests/unit/resources/v2/test_business.py @@ -36,7 +36,7 @@ from legal_api.services import flags from legal_api.utils.datetime import datetime from tests import integration_affiliation -from tests.unit.models import factory_batch, factory_batch_processing, factory_business, factory_pending_filing +from tests.unit.models import factory_batch, factory_batch_processing, factory_business, factory_filing, factory_pending_filing from tests.unit.services.warnings import create_business from tests.unit.services.utils import create_header from tests.unit.models import factory_completed_filing @@ -767,7 +767,7 @@ def _create_affiliation_mapping_draft(identifier, def test_post_affiliation_mappings_migrated_business_without_bootstrap(session, client, jwt): """Assert that direct business identifiers return a mapping even without bootstrap-linked filings.""" identifier = 'BC1328381' - factory_business_model(legal_name=identifier + 'name', + business = factory_business_model(legal_name=identifier + 'name', identifier=identifier, founding_date=datetime.utcfromtimestamp(0), last_ledger_timestamp=datetime.utcfromtimestamp(0), @@ -776,6 +776,11 @@ def test_post_affiliation_mappings_migrated_business_without_bootstrap(session, tax_id=None, dissolution_date=None, legal_type=Business.LegalTypes.BCOMP.value) + factory_filing(business,{'filing': { + 'header': { + 'name': 'lear_tombstone' + } + }}, filing_type='lear_tombstone') rv = client.post('/api/v2/businesses/search/affiliation_mappings', json={'identifiers': [identifier]}, @@ -835,7 +840,7 @@ def test_post_affiliation_mappings_mixed_direct_and_nr_identifiers(session, clie nr_number = 'NR 1234568' bootstrap_identifier = 'Tb31yQIuC3' - factory_business_model(legal_name=business_identifier + 'name', + business = factory_business_model(legal_name=business_identifier + 'name', identifier=business_identifier, founding_date=datetime.utcfromtimestamp(0), last_ledger_timestamp=datetime.utcfromtimestamp(0), @@ -844,6 +849,12 @@ def test_post_affiliation_mappings_mixed_direct_and_nr_identifiers(session, clie tax_id=None, dissolution_date=None, legal_type=Business.LegalTypes.BCOMP.value) + + factory_filing(business,{'filing': { + 'header': { + 'name': 'lear_tombstone' + } + }}, filing_type='lear_tombstone') _create_affiliation_mapping_draft(identifier=bootstrap_identifier, filing_name='registration',