Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion legal-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "legal-api"
version = "3.1.42"
version = "3.1.44"
description = ""
authors = [
{name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"}
Expand Down
251 changes: 146 additions & 105 deletions legal-api/src/legal_api/resources/v2/business/business_tasks.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,9 @@
from legal_api.services import STAFF_ROLE, colin, doc_service, flags, namex
from legal_api.services.permissions import ListActionsPermissionsAllowed, PermissionService
from legal_api.services.request_context import get_request_context
from legal_api.services.utils import get_date, get_str
from legal_api.services.utils import NO_POSTAL_CODE_COUNTRY_CODES, get_date, get_str
from legal_api.utils.auth import jwt

NO_POSTAL_CODE_COUNTRY_CODES = {
"AO", "AG", "AW", "BS", "BZ", "BJ", "BM", "BO", "BQ", "BW", "BF", "BI",
"CM", "CF", "TD", "KM", "CG", "CD", "CK", "CI", "CW", "DJ", "DM", "GQ",
"ER", "FJ", "TF", "GA", "GM", "GH", "GD", "GY", "HM", "HK",
"KI", "KP", "LY", "MO", "MW", "ML", "MR", "NR",
"AN", "NU", "QA", "RW", "KN", "ST", "SC", "SL", "SX", "SB", "SO", "SR", "SY",
"TL", "TG", "TK", "TO", "TT", "TV", "UG", "AE", "VU", "YE", "ZW"
}

# streetAddress, addressCity, and addressCountry no-leading/trailing-whitespace are enforced by
# the schema (business-schemas address pattern). postalCode is not patterned (optional/nullable),
# so it is still whitespace-validated here.
Expand Down
26 changes: 26 additions & 0 deletions legal-api/src/legal_api/services/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,32 @@
from datetime import date

import dpath
import pycountry

# ISO-2 codes of countries that do not use postal codes; mirrors the frontend address rules.
NO_POSTAL_CODE_COUNTRY_CODES = {
"AO", "AG", "AW", "BS", "BZ", "BJ", "BM", "BO", "BQ", "BW", "BF", "BI",
"CM", "CF", "TD", "KM", "CG", "CD", "CK", "CI", "CW", "DJ", "DM", "GQ",
"ER", "FJ", "TF", "GA", "GM", "GH", "GD", "GY", "HM", "HK",
"KI", "KP", "LY", "MO", "MW", "ML", "MR", "NR",
"AN", "NU", "QA", "RW", "KN", "ST", "SC", "SL", "SX", "SB", "SO", "SR", "SY",
"TL", "TG", "TK", "TO", "TT", "TV", "UG", "AE", "VU", "YE", "ZW"
}


def is_postal_code_required(country: str) -> bool:
"""Return whether an address in ``country`` must carry a postal code.

Only countries that resolve to a code in NO_POSTAL_CODE_COUNTRY_CODES are exempt; a missing or
unrecognised country is treated as requiring one.
"""
if not country:
return True
try:
country_code = pycountry.countries.search_fuzzy(country)[0].alpha_2
except LookupError:
return True
return country_code not in NO_POSTAL_CODE_COUNTRY_CODES


def get_date(filing: dict, path: str) -> date:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,47 @@ class BusinessWarningCodes(str, Enum):

NO_START_DATE = "NO_START_DATE"

NO_REGISTERED_OFFICE = "NO_REGISTERED_OFFICE"
NO_REGISTERED_OFFICE_MAILING_ADDRESS = "NO_REGISTERED_OFFICE_MAILING_ADDRESS"
NO_REGISTERED_OFFICE_MAILING_ADDRESS_STREET = "NO_REGISTERED_OFFICE_MAILING_ADDRESS_STREET"
NO_REGISTERED_OFFICE_MAILING_ADDRESS_CITY = "NO_REGISTERED_OFFICE_MAILING_ADDRESS_CITY"
NO_REGISTERED_OFFICE_MAILING_ADDRESS_COUNTRY = "NO_REGISTERED_OFFICE_MAILING_ADDRESS_COUNTRY"
NO_REGISTERED_OFFICE_MAILING_ADDRESS_POSTAL_CODE = "NO_REGISTERED_OFFICE_MAILING_ADDRESS_POSTAL_CODE"
NO_REGISTERED_OFFICE_MAILING_ADDRESS_REGION = "NO_REGISTERED_OFFICE_MAILING_ADDRESS_REGION"
NO_REGISTERED_OFFICE_DELIVERY_ADDRESS = "NO_REGISTERED_OFFICE_DELIVERY_ADDRESS"
NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_STREET = "NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_STREET"
NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_CITY = "NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_CITY"
NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_COUNTRY = "NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_COUNTRY"
NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_POSTAL_CODE = "NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_POSTAL_CODE"
NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_REGION = "NO_REGISTERED_OFFICE_DELIVERY_ADDRESS_REGION"

NO_RECORDS_OFFICE = "NO_RECORDS_OFFICE"
NO_RECORDS_OFFICE_MAILING_ADDRESS = "NO_RECORDS_OFFICE_MAILING_ADDRESS"
NO_RECORDS_OFFICE_MAILING_ADDRESS_STREET = "NO_RECORDS_OFFICE_MAILING_ADDRESS_STREET"
NO_RECORDS_OFFICE_MAILING_ADDRESS_CITY = "NO_RECORDS_OFFICE_MAILING_ADDRESS_CITY"
NO_RECORDS_OFFICE_MAILING_ADDRESS_COUNTRY = "NO_RECORDS_OFFICE_MAILING_ADDRESS_COUNTRY"
NO_RECORDS_OFFICE_MAILING_ADDRESS_POSTAL_CODE = "NO_RECORDS_OFFICE_MAILING_ADDRESS_POSTAL_CODE"
NO_RECORDS_OFFICE_MAILING_ADDRESS_REGION = "NO_RECORDS_OFFICE_MAILING_ADDRESS_REGION"
NO_RECORDS_OFFICE_DELIVERY_ADDRESS = "NO_RECORDS_OFFICE_DELIVERY_ADDRESS"
NO_RECORDS_OFFICE_DELIVERY_ADDRESS_STREET = "NO_RECORDS_OFFICE_DELIVERY_ADDRESS_STREET"
NO_RECORDS_OFFICE_DELIVERY_ADDRESS_CITY = "NO_RECORDS_OFFICE_DELIVERY_ADDRESS_CITY"
NO_RECORDS_OFFICE_DELIVERY_ADDRESS_COUNTRY = "NO_RECORDS_OFFICE_DELIVERY_ADDRESS_COUNTRY"
NO_RECORDS_OFFICE_DELIVERY_ADDRESS_POSTAL_CODE = "NO_RECORDS_OFFICE_DELIVERY_ADDRESS_POSTAL_CODE"
NO_RECORDS_OFFICE_DELIVERY_ADDRESS_REGION = "NO_RECORDS_OFFICE_DELIVERY_ADDRESS_REGION"

NO_DIRECTOR_MAILING_ADDRESS = "NO_DIRECTOR_MAILING_ADDRESS"
NO_DIRECTOR_MAILING_ADDRESS_STREET = "NO_DIRECTOR_MAILING_ADDRESS_STREET"
NO_DIRECTOR_MAILING_ADDRESS_CITY = "NO_DIRECTOR_MAILING_ADDRESS_CITY"
NO_DIRECTOR_MAILING_ADDRESS_COUNTRY = "NO_DIRECTOR_MAILING_ADDRESS_COUNTRY"
NO_DIRECTOR_MAILING_ADDRESS_POSTAL_CODE = "NO_DIRECTOR_MAILING_ADDRESS_POSTAL_CODE"
NO_DIRECTOR_MAILING_ADDRESS_REGION = "NO_DIRECTOR_MAILING_ADDRESS_REGION"
NO_DIRECTOR_DELIVERY_ADDRESS = "NO_DIRECTOR_DELIVERY_ADDRESS"
NO_DIRECTOR_DELIVERY_ADDRESS_STREET = "NO_DIRECTOR_DELIVERY_ADDRESS_STREET"
NO_DIRECTOR_DELIVERY_ADDRESS_CITY = "NO_DIRECTOR_DELIVERY_ADDRESS_CITY"
NO_DIRECTOR_DELIVERY_ADDRESS_COUNTRY = "NO_DIRECTOR_DELIVERY_ADDRESS_COUNTRY"
NO_DIRECTOR_DELIVERY_ADDRESS_POSTAL_CODE = "NO_DIRECTOR_DELIVERY_ADDRESS_POSTAL_CODE"
NO_DIRECTOR_DELIVERY_ADDRESS_REGION = "NO_DIRECTOR_DELIVERY_ADDRESS_REGION"

MULTIPLE_ANNUAL_REPORTS_NOT_FILED = "MULTIPLE_ANNUAL_REPORTS_NOT_FILED"
TRANSITION_NOT_FILED = "TRANSITION_NOT_FILED"
TRANSITION_NOT_FILED_AFTER_12_MONTH_RESTORATION = "TRANSITION_NOT_FILED_AFTER_12_MONTH_RESTORATION"
Expand All @@ -86,6 +127,17 @@ class BusinessWarningReferers(str, Enum):
BUSINESS_OFFICE = "BUSINESS_OFFICE"
BUSINESS_PARTY = "BUSINESS_PARTY"
COMPLETING_PARTY = "COMPLETING_PARTY"
REGISTERED_OFFICE = "REGISTERED_OFFICE"
RECORDS_OFFICE = "RECORDS_OFFICE"
DIRECTOR = "DIRECTOR"


# referers whose delivery address must carry a region
OFFICE_REFERERS = (
BusinessWarningReferers.BUSINESS_OFFICE,
BusinessWarningReferers.REGISTERED_OFFICE,
BusinessWarningReferers.RECORDS_OFFICE
)


class BusinessWarnings(str, Enum):
Expand All @@ -102,6 +154,35 @@ class BusinessWarnings(str, Enum):
WARNING_MESSAGE_BASE = {"warningType": WarningType.MISSING_REQUIRED_BUSINESS_INFO}


def _address_warnings(code_prefix: str, label: str) -> dict:
"""Build the mailing and delivery address warnings for a referer (e.g. "REGISTERED_OFFICE", "Registered office")."""
addresses = {}
for address_type in ("mailing", "delivery"):
code_base = f"NO_{code_prefix}_{address_type.upper()}_ADDRESS"
addresses[address_type] = {
BusinessWarnings.NO_ADDRESS: {
**WARNING_MESSAGE_BASE,
"code": BusinessWarningCodes(code_base),
"message": f"{label} {address_type} address is required."
},
**{
BusinessWarnings(f"NO_ADDRESS_{field}"): {
**WARNING_MESSAGE_BASE,
"code": BusinessWarningCodes(f"{code_base}_{field}"),
"message": f"{field_label} is required for {label.lower()} {address_type} address."
}
for field, field_label in (
("STREET", "Street"),
("CITY", "City"),
("COUNTRY", "Country"),
("POSTAL_CODE", "Postal code"),
("REGION", "Region")
)
}
}
return {"addresses": addresses}


REFERER_WARNINGS_MAPPING = {
BusinessWarningReferers.BUSINESS_OFFICE: {
"addresses": {
Expand Down Expand Up @@ -242,7 +323,10 @@ class BusinessWarnings(str, Enum):
}
}
}
}
},
BusinessWarningReferers.REGISTERED_OFFICE: _address_warnings("REGISTERED_OFFICE", "Registered office"),
BusinessWarningReferers.RECORDS_OFFICE: _address_warnings("RECORDS_OFFICE", "Records office"),
BusinessWarningReferers.DIRECTOR: _address_warnings("DIRECTOR", "Director")
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,91 @@

"""Business checks for corps."""

from business_model.models import Business
from legal_api.services.warnings.business.business_checks import BusinessWarningCodes, WarningType
from business_model.models import Address, Business, OfficeType, PartyRole
from legal_api.services.warnings.business.business_checks import (
WARNING_MESSAGE_BASE,
BusinessWarningCodes,
BusinessWarningReferers,
WarningType,
)
from legal_api.services.warnings.business.business_checks.firms import check_address

# office type -> (warning referer, missing office code, missing office message)
OFFICE_CHECKS = {
OfficeType.REGISTERED: (BusinessWarningReferers.REGISTERED_OFFICE,
BusinessWarningCodes.NO_REGISTERED_OFFICE,
"A registered office is required."),
OfficeType.RECORDS: (BusinessWarningReferers.RECORDS_OFFICE,
BusinessWarningCodes.NO_RECORDS_OFFICE,
"A records office is required.")
}


def check_business(business: Business, is_staff: bool = False) -> list:
"""Check business data."""
result = []

result.extend(check_offices(business))
result.extend(check_directors(business))
result.extend(check_amalgamating_business(business, is_staff))
result.extend(check_transition_application(business))

return result


def _with_data(warnings: list, data: dict) -> list:
"""Return copies of the warnings carrying ``data`` so the UI can identify the office/party to fix."""
return [{**warning, "data": data} for warning in warnings]


def check_offices(business: Business) -> list:
"""Check for missing registered office and records office address data."""
result = []

for office_type, (referer, no_office_code, no_office_message) in OFFICE_CHECKS.items():
data = {"officeType": office_type}
office = business.offices \
.filter_by(office_type=office_type, deactivated_date=None) \
.first()

if not office:
result.append({
**WARNING_MESSAGE_BASE,
"code": no_office_code,
"message": no_office_message,
"data": data
})
continue

addresses = office.addresses.all()
mailing_address = next((x for x in addresses if x.address_type == Address.MAILING), None)
result.extend(_with_data(check_address(mailing_address, Address.MAILING, referer), data))

delivery_address = next((x for x in addresses if x.address_type == Address.DELIVERY), None)
result.extend(_with_data(check_address(delivery_address, Address.DELIVERY, referer), data))

return result


def check_directors(business: Business) -> list:
"""Check for missing address data on active directors."""
result = []

director_roles = business.party_roles \
.filter(PartyRole.role == PartyRole.RoleTypes.DIRECTOR.value) \
.filter(PartyRole.cessation_date.is_(None))

for party_role in director_roles:
party = party_role.party
data = {"partyId": party.id}
result.extend(_with_data(
check_address(party.mailing_address, Address.MAILING, BusinessWarningReferers.DIRECTOR), data))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that this check seems to flag a missing postal code unconditionally, whereas postal-code requirements for director addresses are country-specific. Is this accounted for in the new corporation warning logic? Same applies for delivery address check below.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, it wasn't. check_address flagged a missing postal code regardless of country. Now it goes through the same NO_POSTAL_CODE_COUNTRY_CODES exemption as _validate_postal_code (moved the constant to services/utils.py so the warnings package can share it without importing the filing validators). Since the check is shared, firm party/office warnings get the same fix. Added cases for CA/US/HK/AE/unresolvable country on both the firms and corps side.

result.extend(_with_data(
check_address(party.delivery_address, Address.DELIVERY, BusinessWarningReferers.DIRECTOR), data))

return result


def check_transition_application(business: Business) -> list:
"""Check if a business is currently pending a post restoration transition application."""
result = []
Expand Down Expand Up @@ -55,7 +126,7 @@ def check_amalgamating_business(business: Business, is_staff: bool = False) -> l
name_request = amalgamation_application.get("nameRequest", {})
# legalName is only present when the resulting business will be named
resulting_business_name = name_request.get("legalName")

warning = {
"code": "AMALGAMATING_BUSINESS",
"message": "This business is part of a future effective amalgamation.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
from typing import Final

from business_model.models import Address, Business, Filing, Office, Party, PartyRole
from legal_api.services.utils import is_postal_code_required

from . import (
OFFICE_REFERERS,
WARNING_MESSAGE_BASE,
BusinessWarningCodes,
BusinessWarningReferers,
Expand Down Expand Up @@ -254,12 +256,12 @@ def check_address(address: Address,
result.append(get_address_business_warning(referer,
address_type,
BusinessWarnings.NO_ADDRESS_COUNTRY))
if not address.postal_code:
if not address.postal_code and is_postal_code_required(address.country):
result.append(get_address_business_warning(referer,
address_type,
BusinessWarnings.NO_ADDRESS_POSTAL_CODE))

if (referer == BusinessWarningReferers.BUSINESS_OFFICE
if (referer in OFFICE_REFERERS
and address_type == Address.DELIVERY
and not address.region):
result.append(get_address_business_warning(referer,
Expand Down
6 changes: 3 additions & 3 deletions legal-api/tests/unit/resources/v2/test_business.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,12 +579,12 @@ def test_get_business_with_allowed_filings(session, client, jwt):
('NO_WARNINGS_EXIST_NO_MISSING_DATA', 'SP', 'FM0000003', False, False),
('NO_WARNINGS_EXIST_NO_MISSING_DATA', 'GP', 'FM0000004', False, False),
('NO_WARNINGS_NON_FIRM', 'CP', 'CP7654321', True, False),
('NO_WARNINGS_NON_FIRM', 'BEN', 'CP7654321', True, False),
('NO_WARNINGS_NON_FIRM', 'BC', 'BC7654321', True, False),
('WARNINGS_EXIST_MISSING_DATA_CORPS', 'BEN', 'BC7654321', True, True),
('WARNINGS_EXIST_MISSING_DATA_CORPS', 'BC', 'BC7654321', True, True),
])
def test_get_business_with_incomplete_info(session, client, jwt, test_name, legal_type, identifier, has_missing_business_info,
missing_business_info_warning_expected):
"""Assert that SP/GPs with missing business info is populating warnings list."""
"""Assert that SP/GPs and corps with missing business info are populating warnings list."""

if has_missing_business_info:
business = factory_business(entity_type=legal_type, identifier=identifier)
Expand Down
Loading
Loading