From 4a7f6a33e2d51b6bbbf5bbe8cf6c38577d5fe3d4 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 11 Feb 2026 23:53:37 -0800 Subject: [PATCH] Validation on director names Signed-off-by: Lucas --- .../validations/change_of_directors.py | 69 ++++++++++++++++-- .../filings/validations/common_validations.py | 3 +- .../test_validation_directors_names.py | 72 +++++++++++++++++++ 3 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_directors_names.py diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_directors.py b/legal-api/src/legal_api/services/filings/validations/change_of_directors.py index 47a843eb17..05e57780b1 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_directors.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_directors.py @@ -15,17 +15,15 @@ from http import HTTPStatus import pycountry -from flask_babel import _ as babel # noqa: N813, I004, I001; importing camelcase '_' as a name +from flask_babel import _ as babel # importing camelcase '_' as a name from legal_api.errors import Error from legal_api.models import Address, Business, Filing -from legal_api.services.filings.validations.common_validations import validate_parties_addresses +from legal_api.services.filings.validations.common_validations import PARTY_NAME_MAX_LENGTH, validate_parties_addresses from legal_api.services.utils import get_str from legal_api.utils.datetime import date, datetime from legal_api.utils.legislation_datetime import LegislationDatetime -# noqa: I003; needed as the linter gets confused from the babel override above. - def validate(business: Business, cod: dict) -> Error: """Validate the Change of Directors filing.""" @@ -41,6 +39,10 @@ def validate(business: Business, cod: dict) -> Error: if msg_appointment_date: msg += msg_appointment_date + msg_directors_names = validate_directors_name(cod) + if msg_directors_names: + msg += msg_directors_names + msg_directors_addresses = validate_directors_addresses(business, cod) if msg_directors_addresses: msg += msg_directors_addresses @@ -267,3 +269,62 @@ def validate_effective_date(business: Business, cod: dict) -> list: msg.append({"error": babel("Effective date cannot be before another Change of Director filing.")}) return msg + +def validate_directors_name(cod: dict) -> list: + """Return error messages if a director's name fields are invalid. + + Rules: + - firstName and lastName are required (non-empty) for all directors. + - prevFirstName and prevLastName are required when "nameChanged" is in actions. + - No leading or trailing whitespace. + - All name fields have a maximum length of 30 characters. + """ + msg = [] + filing_type = "changeOfDirectors" + directors = cod["filing"][filing_type]["directors"] + + name_fields = ["firstName", "middleInitial", "lastName", + "prevFirstName", "prevMiddleInitial", "prevLastName"] + required_fields = ["firstName", "lastName"] + name_changed_required_fields = ["prevFirstName", "prevLastName"] + + for idx, director in enumerate(directors): + officer = director.get("officer", {}) + actions = director.get("actions", []) + is_name_changed = "nameChanged" in actions + + for field in name_fields: + value = officer.get(field) + path = f"/filing/changeOfDirectors/directors/{idx}/officer/{field}" + + if field in required_fields and (not value or not value.strip()): + msg.append({ + "error": babel(f"Director {field} is required."), + "path": path + }) + continue + + # Check prev first/last required when nameChanged + if field in name_changed_required_fields and is_name_changed and (not value or not value.strip()): + msg.append({ + "error": babel(f"Director {field} is required when name has changed."), + "path": path + }) + continue + + if value: + # No leading or trailing whitespace + if value != value.strip(): + msg.append({ + "error": babel(f"Director {field} cannot have leading or trailing whitespace."), + "path": path + }) + + # Max length + if len(value) > PARTY_NAME_MAX_LENGTH: + msg.append({ + "error": babel(f"Director {field} cannot be longer than {PARTY_NAME_MAX_LENGTH} characters."), + "path": path + }) + + return msg diff --git a/legal-api/src/legal_api/services/filings/validations/common_validations.py b/legal-api/src/legal_api/services/filings/validations/common_validations.py index b62165bb02..a68da17c1c 100644 --- a/legal-api/src/legal_api/services/filings/validations/common_validations.py +++ b/legal-api/src/legal_api/services/filings/validations/common_validations.py @@ -50,6 +50,7 @@ "postalCode", ) +PARTY_NAME_MAX_LENGTH = 30 # Share structure constants EXCLUDED_WORDS_FOR_CLASS = ["share", "shares", "value"] @@ -407,7 +408,7 @@ def validate_party_name(party: dict, party_path: str, legal_type: str) -> list: msg = [] custom_allowed_max_length = 20 - last_name_max_length = 30 + last_name_max_length = PARTY_NAME_MAX_LENGTH officer = party["officer"] party_type = officer["partyType"] party_roles = [x.get("roleType") for x in party["roles"]] diff --git a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_directors_names.py b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_directors_names.py new file mode 100644 index 0000000000..4126cbb99c --- /dev/null +++ b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_directors_names.py @@ -0,0 +1,72 @@ +import copy +import pytest +from http import HTTPStatus +from legal_api.services.filings.validations.change_of_directors import validate_directors_name + +# Minimal valid director structure for reuse +def make_director(**kwargs): + base = { + "actions": [], + "officer": { + "firstName": "John", + "middleInitial": "Q", + "lastName": "Public", + "prevFirstName": "", + "prevMiddleInitial": "", + "prevLastName": "" + } + } + for k, v in kwargs.items(): + if k == "officer": + base["officer"].update(v) + else: + base[k] = v + return base + +@pytest.mark.parametrize( + "test_name,directors,expected_msgs", + [ + ("valid_minimal", [make_director()], []), + ("missing_firstName", [make_director(officer={"firstName": ""})], [ + {"error": "Director firstName is required.", "path": "/filing/changeOfDirectors/directors/0/officer/firstName"} + ]), + ("missing_lastName", [make_director(officer={"lastName": ""})], [ + {"error": "Director lastName is required.", "path": "/filing/changeOfDirectors/directors/0/officer/lastName"} + ]), + ("nameChanged_missing_prevFirstName", [make_director(actions=["nameChanged"], officer={"prevFirstName": "", "prevLastName": "Smith"})], [ + {"error": "Director prevFirstName is required when name has changed.", "path": "/filing/changeOfDirectors/directors/0/officer/prevFirstName"} + ]), + ("nameChanged_missing_prevLastName", [make_director(actions=["nameChanged"], officer={"prevFirstName": "Jane", "prevLastName": ""})], [ + {"error": "Director prevLastName is required when name has changed.", "path": "/filing/changeOfDirectors/directors/0/officer/prevLastName"} + ]), + ("leading_whitespace", [make_director(officer={"firstName": " John"})], [ + {"error": "Director firstName cannot have leading or trailing whitespace.", "path": "/filing/changeOfDirectors/directors/0/officer/firstName"} + ]), + ("trailing_whitespace", [make_director(officer={"lastName": "Public "})], [ + {"error": "Director lastName cannot have leading or trailing whitespace.", "path": "/filing/changeOfDirectors/directors/0/officer/lastName"} + ]), + ("over_max_length", [make_director(officer={"firstName": "A"*31})], [ + {"error": "Director firstName cannot be longer than 30 characters.", "path": "/filing/changeOfDirectors/directors/0/officer/firstName"} + ]), + ("multiple_directors", [ + make_director(), + make_director(officer={"firstName": "", "lastName": ""}) + ], [ + {"error": "Director firstName is required.", "path": "/filing/changeOfDirectors/directors/1/officer/firstName"}, + {"error": "Director lastName is required.", "path": "/filing/changeOfDirectors/directors/1/officer/lastName"} + ]), + ] +) +def test_validate_directors_name(test_name, directors, expected_msgs): + cod = { + "filing": { + "changeOfDirectors": { + "directors": directors + } + } + } + msgs = validate_directors_name(cod) + msgs_simple = [ + {"error": m["error"].replace("Director ", "Director "), "path": m["path"]} for m in msgs + ] + assert msgs_simple == expected_msgs