diff --git a/legal-api/src/legal_api/services/filings/validations/alteration.py b/legal-api/src/legal_api/services/filings/validations/alteration.py index 2061c10553..ef6cdc9183 100644 --- a/legal-api/src/legal_api/services/filings/validations/alteration.py +++ b/legal-api/src/legal_api/services/filings/validations/alteration.py @@ -24,6 +24,7 @@ from legal_api.services.utils import get_bool, get_str from .common_validations import ( + validate_contact_point_email, validate_court_order, validate_effective_date, validate_name_request, @@ -63,6 +64,7 @@ def validate(business: Business, filing: dict) -> Error: # pylint: disable=too- msg.extend(err) msg.extend(validate_effective_date(filing)) + msg.extend(validate_contact_point_email(filing, "alteration")) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) diff --git a/legal-api/src/legal_api/services/filings/validations/amalgamation_application.py b/legal-api/src/legal_api/services/filings/validations/amalgamation_application.py index 50915e64e2..f0521c16b3 100644 --- a/legal-api/src/legal_api/services/filings/validations/amalgamation_application.py +++ b/legal-api/src/legal_api/services/filings/validations/amalgamation_application.py @@ -23,6 +23,7 @@ from legal_api.errors import Error from legal_api.services import STAFF_ROLE, flags from legal_api.services.filings.validations.common_validations import ( + validate_contact_point_email, validate_court_order, validate_effective_date, validate_foreign_jurisdiction, @@ -104,6 +105,7 @@ def validate(amalgamation_json: dict, account_id) -> Error | None: msg.extend(err) msg.extend(validate_effective_date(amalgamation_json)) + msg.extend(validate_contact_point_email(amalgamation_json, filing_type)) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_registration.py b/legal-api/src/legal_api/services/filings/validations/change_of_registration.py index a633388ee3..ec16e10ce9 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_registration.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_registration.py @@ -22,6 +22,7 @@ from legal_api.services.filings.validations.common_validations import ( find_updated_keys_for_firms, is_officer_proprietor_replace_valid, + validate_contact_point_email, validate_name_request, validate_offices_addresses, validate_parties_addresses, @@ -85,6 +86,7 @@ def validate(business: Business, filing: dict) -> Error | None: msg.extend(validate_naics(filing, filing_type)) msg.extend(validate_registration_court_order(filing, filing_type)) + msg.extend(validate_contact_point_email(filing, filing_type)) if msg: return Error(HTTPStatus.BAD_REQUEST, 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 0ad800de2d..28f3503e41 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 @@ -1052,6 +1052,22 @@ def _validate_postal_code( return None +_EMAIL_PATTERN = re.compile( + r'^(([^<>()\[\]\\.,;:\s@"]+' + r'(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))' + r'@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])' + r'|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$' +) + + +def validate_contact_point_email(filing_json: dict, filing_type: str) -> list: + """Validate contactPoint email format.""" + email = filing_json["filing"][filing_type].get("contactPoint", {}).get("email") + if email and not _EMAIL_PATTERN.fullmatch(email): + return [{"error": "Invalid email address.", "path": f"/filing/{filing_type}/contactPoint/email"}] + return [] + + def validate_phone_number(filing_json: dict, legal_type: str, filing_type: str) -> list: """Validate phone number.""" if legal_type not in Business.CORPS: diff --git a/legal-api/src/legal_api/services/filings/validations/continuation_in.py b/legal-api/src/legal_api/services/filings/validations/continuation_in.py index d36db50073..a6210b9484 100644 --- a/legal-api/src/legal_api/services/filings/validations/continuation_in.py +++ b/legal-api/src/legal_api/services/filings/validations/continuation_in.py @@ -22,6 +22,7 @@ from legal_api.errors import Error from legal_api.services import colin, flags from legal_api.services.filings.validations.common_validations import ( + validate_contact_point_email, validate_court_order, validate_effective_date, validate_foreign_jurisdiction, @@ -89,6 +90,8 @@ def validate(filing_json: dict) -> Error | None: # pylint: disable=too-many-bra if err: msg.extend(err) + msg.extend(validate_contact_point_email(filing_json, filing_type)) + if msg: return Error(HTTPStatus.BAD_REQUEST, msg) return None diff --git a/legal-api/src/legal_api/services/filings/validations/correction.py b/legal-api/src/legal_api/services/filings/validations/correction.py index 7e3f587496..b2a2df7b7d 100644 --- a/legal-api/src/legal_api/services/filings/validations/correction.py +++ b/legal-api/src/legal_api/services/filings/validations/correction.py @@ -25,6 +25,7 @@ from legal_api.errors import Error from legal_api.services import STAFF_ROLE, SYSTEM_ROLE, NaicsService from legal_api.services.filings.validations.common_validations import ( + validate_contact_point_email, validate_court_order, validate_name_request, validate_offices_addresses, @@ -108,6 +109,8 @@ def validate(business: Business, filing: dict) -> Error: elif business.legal_type == Business.LegalTypes.COOP.value: _validate_special_resolution_correction(filing, business.legal_type, msg) + msg.extend(validate_contact_point_email(filing, filing_type)) + if msg: return Error(HTTPStatus.BAD_REQUEST, msg) diff --git a/legal-api/src/legal_api/services/filings/validations/registration.py b/legal-api/src/legal_api/services/filings/validations/registration.py index 4b7f0a449d..3b1f786080 100644 --- a/legal-api/src/legal_api/services/filings/validations/registration.py +++ b/legal-api/src/legal_api/services/filings/validations/registration.py @@ -27,6 +27,7 @@ from legal_api.errors import Error from legal_api.services import STAFF_ROLE, NaicsService, flags from legal_api.services.filings.validations.common_validations import ( + validate_contact_point_email, validate_court_order, validate_name_request, validate_offices_addresses, @@ -78,6 +79,7 @@ def validate(registration_json: dict) -> Error | None: msg.extend(validate_offices(registration_json)) msg.extend(validate_offices_addresses(registration_json, filing_type)) msg.extend(validate_registration_court_order(registration_json)) + msg.extend(validate_contact_point_email(registration_json, filing_type)) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) diff --git a/legal-api/src/legal_api/services/filings/validations/restoration.py b/legal-api/src/legal_api/services/filings/validations/restoration.py index 178d0a46a3..6bd862a314 100644 --- a/legal-api/src/legal_api/services/filings/validations/restoration.py +++ b/legal-api/src/legal_api/services/filings/validations/restoration.py @@ -22,6 +22,7 @@ from business_model.models import Business, Filing, PartyRole from legal_api.errors import Error from legal_api.services.filings.validations.common_validations import ( + validate_contact_point_email, validate_court_order, validate_name_request, validate_offices_addresses, @@ -51,6 +52,7 @@ def validate(business: Business, restoration: dict) -> Error | None: msg.extend(validate_contact_point(restoration)) elif restoration_type in ("fullRestoration", "limitedRestorationToFull"): msg.extend(validate_relationship(restoration)) + msg.extend(validate_contact_point_email(restoration, filing_type)) if restoration_type in ("fullRestoration", "limitedRestoration", "limitedRestorationToFull"): name_request = restoration.get("filing", {}).get("restoration", {}).get("nameRequest", {}) diff --git a/legal-api/tests/unit/services/filings/validations/test_common_validations.py b/legal-api/tests/unit/services/filings/validations/test_common_validations.py index 52d505dcc4..c933e4a165 100644 --- a/legal-api/tests/unit/services/filings/validations/test_common_validations.py +++ b/legal-api/tests/unit/services/filings/validations/test_common_validations.py @@ -55,6 +55,7 @@ validate_authorization_received, validate_certify_name, validate_certified_by, + validate_contact_point_email, validate_court_order, validate_foreign_jurisdiction, validate_offices, @@ -1137,6 +1138,35 @@ def test_is_officer_proprietor_replace_valid(session, test_name, legal_type, exi result = is_officer_proprietor_replace_valid(business, filing_json, 'changeOfRegistration') assert result is expected_result +@pytest.mark.parametrize('email, contact_point, expect_error', [ + ('test@example.com', {'email': 'test@example.com'}, False), + ('user.name@domain.com', {'email': 'user.name@domain.com'}, False), + ('user+tag@example.com', {'email': 'user+tag@example.com'}, False), + ('user@bc.gov.ca', {'email': 'user@bc.gov.ca'}, False), + ('a@b.c', {'email': 'a@b.c'}, True), + ('@invalid.com', {'email': '@invalid.com'}, True), + ('test@domain', {'email': 'test@domain'}, True), + ('test @example.com', {'email': 'test @example.com'}, True), + ('test@example.com ', {'email': 'test@example.com '}, True), + (None, {}, False), # no email field + (None, None, False), # no contactPoint +]) +def test_validate_contact_point_email(email, contact_point, expect_error): + """validate_contact_point_email rejects invalid emails and accepts valid ones for all affected filing types.""" + filing_types = [ + 'alteration', 'amalgamationApplication', 'changeOfRegistration', + 'continuationIn', 'correction', 'registration', 'restoration', + ] + for filing_type in filing_types: + filing_body = {'contactPoint': contact_point} if contact_point is not None else {} + filing = {'filing': {filing_type: filing_body}} + result = validate_contact_point_email(filing, filing_type) + assert bool(result) == expect_error, f'Failed for filing_type={filing_type}, email={email}' + if expect_error: + assert result[0]['path'] == f'/filing/{filing_type}/contactPoint/email' + assert result[0]['error'] == 'Invalid email address.' + + @pytest.mark.parametrize('email, is_valid', [ # Valid email formats ('test@example.com', True),