Skip to content
Open
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
8 changes: 4 additions & 4 deletions legal-api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion legal-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ dependencies = [
"blinker (==1.4)",
"pyjwt (==2.8.0)",

"registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.60#egg=registry_schemas",
"registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.62#egg=registry_schemas",
"sql-versioning @ git+https://github.com/bcgov/lear.git@main#subdirectory=python/common/sql-versioning",
"gcp-queue @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/gcp-queue",
"structured-logging @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/structured-logging"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def validate(business: Business, filing: dict) -> Error: # pylint: disable=too-
msg.extend(rules_change_validation(filing))
msg.extend(memorandum_change_validation(filing))

if err := validate_resolution_date_in_share_structure(filing, "alteration"):
msg.append(err)
if err := validate_resolution_date_in_share_structure(filing, "alteration", business):
msg.extend(err)

new_legal_type = filing["filing"]["alteration"].get("business", {}).get("legalType", None)
err = validate_phone_number(filing, new_legal_type or business.legal_type, "alteration")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
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_str
from legal_api.utils.datetime import date
from legal_api.utils.datetime import datetime as dt
from legal_api.utils.legislation_datetime import LegislationDatetime

NO_POSTAL_CODE_COUNTRY_CODES = {
"AO", "AG", "AW", "BS", "BZ", "BJ", "BM", "BO", "BQ", "BW", "BF", "BI",
Expand All @@ -58,22 +60,58 @@
MAX_SHARE_DIGITS = 16


def validate_resolution_date_in_share_structure(filing_json, filing_type) -> Optional[dict]:
"""Has resolution date in share structure when hasRightsOrRestrictions is true."""
def validate_resolution_date_in_share_structure(filing_json, filing_type, business) -> list[dict]:
"""Validate the resolution date of a share structure.

Rules:
- If hasRightsOrRestrictions is true in any share class or series, resolution date is required.
- Only one resolution date is permitted.
- Resolution date cannot be in the future.
- Resolution date cannot be before the business founding date.
"""
share_structure = filing_json["filing"][filing_type].get("shareStructure", {})
share_classes = share_structure.get("shareClasses", [])
resolution_dates = share_structure.get("resolutionDates", [])

err_path = f"/filing/{filing_type}/shareStructure/resolutionDates"

msg = []
if (
(
any(x.get("hasRightsOrRestrictions", False) for x in share_classes) or
any(has_rights_or_restrictions_true_in_share_series(x) for x in share_classes)
) and
len(share_structure.get("resolutionDates", [])) == 0
len(resolution_dates) == 0
):
return {
"error": "Resolution date is required when hasRightsOrRestrictions is true in shareClasses.",
"path": f"/filing/{filing_type}/shareStructure/resolutionDates"
}
return None
msg.append({
"error": "Resolution date is required when hasRightsOrRestrictions is true.",
"path": err_path
})

if len(resolution_dates) > 1:
msg.append({
"error": "Only one resolution date is permitted.",
"path": err_path
})

elif len(resolution_dates) == 1:
resolution_date_leg = date.fromisoformat(resolution_dates[0])
founding_date_leg = LegislationDatetime.as_legislation_timezone(business.founding_date).date()
today_leg = LegislationDatetime.datenow()

if resolution_date_leg > today_leg:
msg.append({
"error": "Resolution date cannot be in the future.",
"path": err_path
})

if resolution_date_leg < founding_date_leg:
msg.append({
"error": "Resolution date cannot be before the business founding date.",
"path": err_path
})

return msg


def has_rights_or_restrictions_true_in_share_series(share_class) -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
validate_parties_addresses,
validate_parties_names,
validate_pdf,
validate_resolution_date_in_share_structure,
validate_share_structure,
)
from legal_api.services.filings.validations.incorporation_application import (
Expand Down Expand Up @@ -84,7 +85,7 @@ def validate(business: Business, filing: dict) -> Error:
if business.legal_type in [Business.LegalTypes.SOLE_PROP.value, Business.LegalTypes.PARTNERSHIP.value]:
_validate_firms_correction(business, filing, business.legal_type, msg)
elif business.legal_type in Business.CORPS:
_validate_corps_correction(filing, business.legal_type, msg)
_validate_corps_correction(business, filing, business.legal_type, msg)
elif business.legal_type == Business.LegalTypes.COOP.value:
_validate_special_resolution_correction(filing, business.legal_type, msg)

Expand All @@ -107,7 +108,7 @@ def _validate_firms_correction(business: Business, filing, legal_type, msg):
msg.extend(validate_naics(business, filing, filing_type))


def _validate_corps_correction(filing_dict, legal_type, msg):
def _validate_corps_correction(business: Business, filing_dict, legal_type, msg):
filing_type = "correction"
if filing_dict.get("filing", {}).get("correction", {}).get("nameRequest", {}).get("nrNumber", None):
msg.extend(validate_name_request(filing_dict, legal_type, filing_type))
Expand All @@ -124,6 +125,10 @@ def _validate_corps_correction(filing_dict, legal_type, msg):
if err:
msg.extend(err)

err = validate_resolution_date_in_share_structure(filing_dict, filing_type, business)
if err:
msg.extend(err)


def _validate_special_resolution_correction(filing_dict, legal_type, msg):
filing_type = "correction"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import copy
from http import HTTPStatus
from unittest.mock import MagicMock, create_autospec, patch
import datedelta
from datetime import date
from flask import g
from legal_api.errors import Error
Expand All @@ -28,6 +29,7 @@
from legal_api.models import Business
from legal_api.services import flags, NameXService
from legal_api.services.filings import validate
from legal_api.utils.datetime import datetime, timezone
from tests.unit.models import factory_business
from tests.unit.services.filings.test_utils import _upload_file
from tests.unit.services.filings.validations import lists_are_equal
Expand Down Expand Up @@ -392,48 +394,79 @@ def test_validate_nr_type(mock_get_parties, session, new_name, legal_type, nr_le
assert HTTPStatus.BAD_REQUEST == err.code
assert err.msg[0]['error'] == err_msg

NOW = datetime(2025, 1, 1, 12, 0, tzinfo=timezone.utc)
FOUNDING_DATE = NOW - datedelta.YEAR

@pytest.mark.parametrize(
'test_name, should_pass, has_rights_or_restrictions, has_rights_or_restrictions_series, resolution_dates', [
('SUCCESS_has_rights_or_restrictions', True, True, False, ['2020-05-23']),
('SUCCESS', True, False, False, []),
('FAILURE', False, True, False, []),
('SUCCESS_series_has_rights_or_restrictions', True, False, True, ['2020-05-23']),
('SUCCESS_series', True, False, False, []),
('FAILURE_series', False, False, True, [])
])
'test_name, has_rights_or_restrictions, has_rights_or_restrictions_series, '
'resolution_dates, expected_code, expected_msg',
[
('SUCCESS_class_has_rights', True, False, ['2024-01-01'], None, None),
('SUCCESS_class_no_rights', False, False, [], None, None),
('SUCCESS_series_has_rights', False, True, ['2024-01-01'], None, None),
('SUCCESS_series_no_rights', False, False, [], None, None),

('FAILURE_class_missing_date', True, False, [], HTTPStatus.BAD_REQUEST, [
{'error': 'Resolution date is required when hasRightsOrRestrictions is true.',
'path': '/filing/alteration/shareStructure/resolutionDates'}
]),
('FAILURE_series_missing_date', False, True, [], HTTPStatus.BAD_REQUEST, [
{'error': 'Resolution date is required when hasRightsOrRestrictions is true.',
'path': '/filing/alteration/shareStructure/resolutionDates'}
]),

('FAILURE_too_many_dates', True, False, ['2024-01-01', '2024-02-01'], HTTPStatus.BAD_REQUEST, [
{'error': 'Only one resolution date is permitted.',
'path': '/filing/alteration/shareStructure/resolutionDates'}
]),

('FAILURE_future_date', True, False, [(NOW + datedelta.DAY).date().isoformat()], HTTPStatus.BAD_REQUEST, [
{'error': 'Resolution date cannot be in the future.',
'path': '/filing/alteration/shareStructure/resolutionDates'}
]),

('FAILURE_before_founding', True, False, [(FOUNDING_DATE - datedelta.DAY).date().isoformat()], HTTPStatus.BAD_REQUEST, [
{'error': 'Resolution date cannot be before the business founding date.',
'path': '/filing/alteration/shareStructure/resolutionDates'}
]),
]
)
@patch.object(PermissionService, 'check_user_permission', MagicMock(return_value=None))
def test_alteration_resolution_date(
session, test_name, should_pass, has_rights_or_restrictions,
has_rights_or_restrictions_series, resolution_dates):
"""Test resolution date in share structure."""
session, test_name, has_rights_or_restrictions,
has_rights_or_restrictions_series, resolution_dates, expected_code, expected_msg):
"""Test resolution date validation in alteration share structure."""
# setup
identifier = 'BC1234567'
business = factory_business(identifier)
business.founding_date = FOUNDING_DATE

f = copy.deepcopy(ALTERATION_FILING_TEMPLATE)
f['filing']['header']['identifier'] = identifier
del f['filing']['alteration']['nameRequest']
del f['filing']['alteration']['business']['legalType']

# set rights/restrictions and resolution dates
f['filing']['alteration']['shareStructure']['shareClasses'][0]['hasRightsOrRestrictions'] = \
has_rights_or_restrictions
f['filing']['alteration']['shareStructure']['shareClasses'][0]['series'][0]['hasRightsOrRestrictions'] = \
has_rights_or_restrictions_series
f['filing']['alteration']['shareStructure']['resolutionDates'] = resolution_dates

err = validate(business, f)
if 'courtOrder' in f['filing']['alteration']:
del f['filing']['alteration']['courtOrder']

if err:
print(err.msg)
# freeze time to ensure deterministic validation
with freeze_time(NOW):
err = validate(business, f)

if should_pass:
# check that validation passed
assert None is err
# validate outcomes
if expected_code:
assert err.code == expected_code
assert lists_are_equal(err.msg, expected_msg)
else:
# check that validation failed
assert err
assert HTTPStatus.BAD_REQUEST == err.code
assert err is None


@patch.object(PermissionService, 'check_user_permission', MagicMock(return_value=None))
def test_alteration_share_classes_optional(session):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"""Test Correction IA validations."""

import copy
import datedelta
from datetime import datetime, timezone
from freezegun import freeze_time
from http import HTTPStatus
from unittest.mock import patch

Expand Down Expand Up @@ -304,3 +307,84 @@ def test_correction_share_class_series_validation(mocker, session, legal_type, h
else:
assert err
assert any('cannot have series when hasRightsOrRestrictions is false' in msg['error'] for msg in err.msg)

NOW = datetime(2025, 1, 1, 12, 0, tzinfo=timezone.utc)
FOUNDING_DATE = NOW - datedelta.YEAR

@pytest.mark.parametrize(
'test_name, has_rights_or_restrictions, has_series, resolution_dates, expected_code, expected_msg',
[
('SUCCESS_class_has_rights', True, False, ['2024-01-01'], None, None),
('SUCCESS_class_no_rights', False, False, [], None, None),
('SUCCESS_series_has_rights', True, True, ['2024-01-01'], None, None),
('SUCCESS_series_no_rights', False, False, [], None, None),

('FAILURE_class_missing_date', True, False, [], HTTPStatus.BAD_REQUEST, [
{'error': 'Resolution date is required when hasRightsOrRestrictions is true.',
'path': '/filing/correction/shareStructure/resolutionDates'}
]),
('FAILURE_series_missing_date', False, True, [], HTTPStatus.BAD_REQUEST, [
{'error': 'Resolution date is required when hasRightsOrRestrictions is true.',
'path': '/filing/correction/shareStructure/resolutionDates'}
]),

('FAILURE_too_many_dates', True, False, ['2024-01-01', '2024-02-01'], HTTPStatus.BAD_REQUEST, [
{'error': 'Only one resolution date is permitted.',
'path': '/filing/correction/shareStructure/resolutionDates'}
]),

('FAILURE_future_date', True, False, [(NOW + datedelta.DAY).date().isoformat()], HTTPStatus.BAD_REQUEST, [
{'error': 'Resolution date cannot be in the future.',
'path': '/filing/correction/shareStructure/resolutionDates'}
]),

('FAILURE_before_founding', True, False, [(FOUNDING_DATE - datedelta.DAY).date().isoformat()], HTTPStatus.BAD_REQUEST, [
{'error': 'Resolution date cannot be before the business founding date.',
'path': '/filing/correction/shareStructure/resolutionDates'}
]),
]
)
def test_correction_resolution_date(mocker, session, test_name, has_rights_or_restrictions,
has_series, resolution_dates, expected_code, expected_msg):
"""Test share class/series resolution date validation in correction filings."""
mocker.patch('legal_api.utils.auth.jwt.validate_roles', return_value=False)
identifier = 'BC1234567'
business = factory_business(identifier, entity_type='BC')
business.founding_date = FOUNDING_DATE

corrected_filing = factory_completed_filing(business, INCORPORATION_APPLICATION)

filing = copy.deepcopy(CORRECTION)
filing['filing']['header']['identifier'] = identifier
filing['filing']['correction']['correctedFilingId'] = corrected_filing.id
del filing['filing']['correction']['commentOnly']

# Share structure setup
filing['filing']['correction']['shareStructure'] = copy.deepcopy(
INCORPORATION_FILING_TEMPLATE['filing']['incorporationApplication'].get('shareStructure', {})
)
share_class = filing['filing']['correction']['shareStructure']['shareClasses'][0]
share_class['hasRightsOrRestrictions'] = has_rights_or_restrictions

# Series handling
if has_series:
share_class['series'] = share_class.get('series', [{}])
share_class['series'][0]['hasRightsOrRestrictions'] = True
else:
share_class.pop('series', None)

filing['filing']['correction']['shareStructure']['resolutionDates'] = resolution_dates

# Remove the second share class if it exists
share_classes = filing['filing']['correction']['shareStructure']['shareClasses']
if len(share_classes) > 1:
share_classes.pop(1)

with freeze_time(NOW):
err = validate(business, filing)

if expected_code:
assert err
assert any(expected_msg[0]['error'] in e['error'] for e in err.msg)
else:
assert err is None
2 changes: 1 addition & 1 deletion queue_services/business-filer/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "business-filer"
version = "3.0.9"
version = "3.0.10"
description = "Business Registry Filer Service"
authors = [
{name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"}
Expand Down
Loading
Loading