Skip to content

Commit 0f4cf67

Browse files
authored
Business API - update dod allowable rules and dissolution warning (#4042)
* Business API - update dod allowable rules and dissolution warning Signed-off-by: Kial Jinnah <kialj876@gmail.com> * chore: fix lint Signed-off-by: Kial Jinnah <kialj876@gmail.com> * chore: test setup fix Signed-off-by: Kial Jinnah <kialj876@gmail.com> * Update common model Signed-off-by: Kial Jinnah <kialj876@gmail.com> * chore: fix test Signed-off-by: Kial Jinnah <kialj876@gmail.com> --------- Signed-off-by: Kial Jinnah <kialj876@gmail.com>
1 parent ecfc822 commit 0f4cf67

8 files changed

Lines changed: 451 additions & 20 deletions

File tree

legal-api/src/legal_api/models/business.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from .party_role import PartyRole
4646
from .resolution import Resolution # noqa: F401 pylint: disable=unused-import; needed by the SQLAlchemy backref
4747
from .share_class import ShareClass # noqa: F401 pylint: disable=unused-import
48-
from .user import User # noqa: F401 pylint: disable=unused-import; needed by the SQLAlchemy backref
48+
from .user import User, UserRoles # noqa: F401 pylint: disable=unused-import; needed by the SQLAlchemy backref
4949

5050

5151
class Business(db.Model, Versioned): # pylint: disable=too-many-instance-attributes,disable=too-many-public-methods
@@ -555,6 +555,27 @@ def in_dissolution(self):
555555
filter(Batch.batch_type == Batch.BatchType.INVOLUNTARY_DISSOLUTION).\
556556
one_or_none()
557557
return find_in_batch_processing is not None
558+
559+
@property
560+
def public_user_dod_filings(self):
561+
"""Return the list of public user filed delay of dissolution filings for the current in progress dissolution process."""
562+
batch_processings: list[BatchProcessing] = BatchProcessing.find_by(business_id=self.id)
563+
if self.in_dissolution and len(batch_processings):
564+
most_recent_batch_processing = batch_processings[0]
565+
dissolution_start_date = most_recent_batch_processing.created_date
566+
relevant_filings: list[Filing] = Filing.get_filings_by_status(
567+
self.id,
568+
[Filing.Status.COMPLETED, Filing.Status.PENDING, Filing.Status.PAID],
569+
dissolution_start_date
570+
)
571+
return [
572+
filing for filing in relevant_filings
573+
if filing.filing_type == "dissolution"
574+
and filing.filing_sub_type == "delay"
575+
and not filing.submitter_roles in [UserRoles.staff, UserRoles.system]
576+
]
577+
578+
return []
558579

559580
@property
560581
def is_tombstone(self):

legal-api/src/legal_api/services/authz.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
PUBLIC_USER = "public_user"
4646
ACCOUNT_IDENTITY = "account_identity"
4747

48+
MAX_PUBLIC_USER_DOD_FILINGS = 2
49+
4850

4951
class BusinessBlocker(str, Enum):
5052
"""Define an enum for business level blocker checks."""
@@ -60,6 +62,7 @@ class BusinessBlocker(str, Enum):
6062
IN_DISSOLUTION = "IN_DISSOLUTION"
6163
IN_LIQUIDATION = "IN_LIQUIDATION"
6264
FILING_WITHDRAWAL = "FILING_WITHDRAWAL"
65+
MAX_DISSOLUTION_DELAYS_REACHED = "MAX_DISSOLUTION_DELAYS_REACHED"
6366

6467

6568
class BusinessRequirement(str, Enum):
@@ -580,8 +583,7 @@ def get_allowable_filings_dict():
580583
"delay": {
581584
"legalTypes": ["CP", "BC", "BEN", "CC", "ULC", "C", "CBEN", "CUL", "CCC"],
582585
"blockerChecks": {
583-
"maxFilings": {"dissolution.delay": 2},
584-
"business": [BusinessBlocker.DEFAULT],
586+
"business": [BusinessBlocker.DEFAULT, BusinessBlocker.MAX_DISSOLUTION_DELAYS_REACHED],
585587
"validBusiness": [BusinessBlocker.IN_DISSOLUTION]
586588
}
587589
}
@@ -913,7 +915,8 @@ def business_blocker_check(business: Business, is_ignore_draft_blockers: bool =
913915
BusinessBlocker.AMALGAMATING_BUSINESS: False,
914916
BusinessBlocker.IN_DISSOLUTION: False,
915917
BusinessBlocker.IN_LIQUIDATION: False,
916-
BusinessBlocker.FILING_WITHDRAWAL: False
918+
BusinessBlocker.FILING_WITHDRAWAL: False,
919+
BusinessBlocker.MAX_DISSOLUTION_DELAYS_REACHED: False
917920
}
918921

919922
if not business:
@@ -942,6 +945,9 @@ def business_blocker_check(business: Business, is_ignore_draft_blockers: bool =
942945
if has_notice_of_withdrawal_filing_blocker(business, is_ignore_draft_blockers):
943946
business_blocker_checks[BusinessBlocker.FILING_WITHDRAWAL] = True
944947

948+
if len(business.public_user_dod_filings) >= MAX_PUBLIC_USER_DOD_FILINGS:
949+
business_blocker_checks[BusinessBlocker.MAX_DISSOLUTION_DELAYS_REACHED] = True
950+
945951
return business_blocker_checks
946952

947953

legal-api/src/legal_api/services/warnings/business/business_checks/involuntary_dissolution.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def check_business(business: Business) -> list:
6262
elif dis_details.ar_overdue:
6363
result.append(ar_overdue_warning)
6464

65-
data = _get_modified_warning_data(batch_processing)
65+
data = _get_modified_warning_data(batch_processing, len(business.public_user_dod_filings))
6666

6767
result.append({
6868
"code": BusinessWarningCodes.DISSOLUTION_IN_PROGRESS,
@@ -74,22 +74,26 @@ def check_business(business: Business) -> list:
7474
return result
7575

7676

77-
def _get_modified_warning_data(batch_processing: BatchProcessing) -> dict:
77+
def _get_modified_warning_data(batch_processing: BatchProcessing, user_delays: int = 0) -> dict:
7878
"""Return involuntary disssolution warning data based on rules."""
7979
meta_data = batch_processing.meta_data if batch_processing.meta_data else {}
8080

8181
trigger_date = batch_processing.trigger_date
8282
current_date = datetime.utcnow()
8383
modified_target_date = None
84+
target_stage_2_date = None
8485
if batch_processing.step == BatchProcessing.BatchProcessingStep.WARNING_LEVEL_1:
8586
modified_target_date = max(current_date, trigger_date) + datedelta(days=current_app.config.get("STAGE_2_DELAY"))
87+
target_stage_2_date = max(current_date, trigger_date)
8688
elif batch_processing.step == BatchProcessing.BatchProcessingStep.WARNING_LEVEL_2:
8789
modified_target_date = max(current_date, trigger_date)
8890

8991
if modified_target_date:
9092
meta_data = {
9193
**meta_data,
92-
"targetDissolutionDate": modified_target_date.date().isoformat()
94+
"userDelays": user_delays,
95+
"targetDissolutionDate": modified_target_date.date().isoformat(),
96+
**({"targetStage2Date": target_stage_2_date.date().isoformat()} if target_stage_2_date else {})
9397
}
9498

9599
return meta_data

legal-api/tests/unit/services/test_authorization.py

Lines changed: 191 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from http import HTTPStatus
2424

2525
import pytest
26+
from datedelta import datedelta
2627
from unittest.mock import patch, PropertyMock
2728
from flask import g, jsonify
2829
from registry_schemas.example_data import (
@@ -50,8 +51,9 @@
5051
authorized, is_allowed, get_allowed, get_allowed_filings, get_allowable_actions
5152
from legal_api.services.permissions import PermissionService
5253
from legal_api.services.warnings.business.business_checks import WarningType
54+
from legal_api.utils.datetime import datetime
5355
from tests import integration_authorization, not_github_ci
54-
from tests.unit.models import factory_business, factory_filing, factory_incomplete_statuses, factory_completed_filing
56+
from tests.unit.models import factory_batch, factory_batch_processing, factory_business, factory_filing, factory_incomplete_statuses, factory_completed_filing
5557
from tests.unit.services.utils import create_business, create_header, helper_create_jwt
5658

5759

@@ -3306,15 +3308,24 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me
33063308
business = factory_business(identifier=identifier,
33073309
entity_type=legal_type,
33083310
state=state)
3309-
for _ in range(num_dods):
3311+
for i in range(num_dods):
33103312
dod_filing_json = copy.deepcopy(FILING_TEMPLATE)
33113313
dod_filing_json["filing"]["header"]["name"] = "dissolution"
33123314
dod_filing_json["filing"]["dissolution"] = DELAY_DISSOLUTION
3313-
factory_completed_filing(business, dod_filing_json)
3314-
with patch.object(type(business), 'in_dissolution', new_callable=PropertyMock) as mock_in_dissolution:
3315-
mock_in_dissolution.return_value = True
3316-
filing_types = get_allowed_filings(business, state, legal_type, jwt)
3317-
assert filing_types == expected
3315+
factory_completed_filing(business, dod_filing_json, datetime.utcnow() - datedelta(days=i))
3316+
3317+
batch = factory_batch()
3318+
batch_processing = factory_batch_processing(
3319+
batch_id=batch.id,
3320+
business_id=business.id,
3321+
identifier=identifier,
3322+
trigger_date=datetime.utcnow() + datedelta(days=10)
3323+
)
3324+
batch_processing.created_date = datetime.utcnow() + datedelta(days=-10)
3325+
batch_processing.save()
3326+
3327+
filing_types = get_allowed_filings(business, state, legal_type, jwt)
3328+
assert filing_types == expected
33183329

33193330

33203331
@pytest.mark.parametrize(
@@ -3609,6 +3620,179 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me
36093620
assert allowed_filing_types == expected
36103621

36113622

3623+
@pytest.mark.parametrize(
3624+
'test_name,username,roles,dods,expected',
3625+
[
3626+
# active business - staff user
3627+
('staff_allowed_no_dod_filings', 'staff', [STAFF_ROLE], [],
3628+
expected_lookup([FilingKey.ADMN_FRZE,
3629+
FilingKey.AR_CORPS,
3630+
FilingKey.COA_CORPS,
3631+
FilingKey.COD_CORPS,
3632+
FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT,
3633+
FilingKey.CHANGE_OF_LIQUIDATORS_CEASE,
3634+
FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS,
3635+
FilingKey.CHANGE_OF_LIQUIDATORS_INTENT,
3636+
FilingKey.CHANGE_OF_LIQUIDATORS_REPORT,
3637+
FilingKey.COO_CORPS,
3638+
FilingKey.CHANGE_OF_RECEIVERS_AMEND,
3639+
FilingKey.CHANGE_OF_RECEIVERS_APPOINT,
3640+
FilingKey.CHANGE_OF_RECEIVERS_CEASE,
3641+
FilingKey.CHANGE_OF_RECEIVERS_ADDRESS,
3642+
FilingKey.CORRCTN,
3643+
FilingKey.COURT_ORDER,
3644+
FilingKey.ADM_DISS,
3645+
FilingKey.DELAY_DISS,
3646+
FilingKey.PUT_BACK_OFF,
3647+
FilingKey.REGISTRARS_NOTATION,
3648+
FilingKey.REGISTRARS_ORDER,
3649+
FilingKey.TRANSITION,
3650+
])),
3651+
('staff_allowed_public_user_dod_filings',
3652+
'staff',
3653+
[STAFF_ROLE],
3654+
[{ 'date': datetime.utcnow() + datedelta(days=-3), 'user': 'public' }, { 'date': datetime.utcnow() + datedelta(days=-2), 'user': 'public' }],
3655+
expected_lookup([FilingKey.ADMN_FRZE,
3656+
FilingKey.AR_CORPS,
3657+
FilingKey.COA_CORPS,
3658+
FilingKey.COD_CORPS,
3659+
FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT,
3660+
FilingKey.CHANGE_OF_LIQUIDATORS_CEASE,
3661+
FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS,
3662+
FilingKey.CHANGE_OF_LIQUIDATORS_INTENT,
3663+
FilingKey.CHANGE_OF_LIQUIDATORS_REPORT,
3664+
FilingKey.COO_CORPS,
3665+
FilingKey.CHANGE_OF_RECEIVERS_AMEND,
3666+
FilingKey.CHANGE_OF_RECEIVERS_APPOINT,
3667+
FilingKey.CHANGE_OF_RECEIVERS_CEASE,
3668+
FilingKey.CHANGE_OF_RECEIVERS_ADDRESS,
3669+
FilingKey.CORRCTN,
3670+
FilingKey.COURT_ORDER,
3671+
FilingKey.ADM_DISS,
3672+
FilingKey.DELAY_DISS,
3673+
FilingKey.PUT_BACK_OFF,
3674+
FilingKey.REGISTRARS_NOTATION,
3675+
FilingKey.REGISTRARS_ORDER,
3676+
FilingKey.TRANSITION,
3677+
])),
3678+
('basic_user_allowed_no_dod_filings',
3679+
'basic_user',
3680+
[BASIC_USER],
3681+
[],
3682+
expected_lookup([FilingKey.AR_CORPS,
3683+
FilingKey.COA_CORPS,
3684+
FilingKey.COD_CORPS,
3685+
FilingKey.COO_CORPS,
3686+
FilingKey.DELAY_DISS,
3687+
FilingKey.TRANSITION,
3688+
FilingKey.TRANSPARENCY_REGISTER_ANNUAL,
3689+
FilingKey.TRANSPARENCY_REGISTER_CHANGE,
3690+
FilingKey.TRANSPARENCY_REGISTER_INITIAL])),
3691+
('basic_user_allowed_staff_dod_filings',
3692+
'basic_user',
3693+
[BASIC_USER],
3694+
[{ 'date': datetime.utcnow() + datedelta(days=-3), 'user': 'staff' }, { 'date': datetime.utcnow() + datedelta(days=-2), 'user': 'staff' }],
3695+
expected_lookup([FilingKey.AR_CORPS,
3696+
FilingKey.COA_CORPS,
3697+
FilingKey.COD_CORPS,
3698+
FilingKey.COO_CORPS,
3699+
FilingKey.DELAY_DISS,
3700+
FilingKey.TRANSITION,
3701+
FilingKey.TRANSPARENCY_REGISTER_ANNUAL,
3702+
FilingKey.TRANSPARENCY_REGISTER_CHANGE,
3703+
FilingKey.TRANSPARENCY_REGISTER_INITIAL])),
3704+
('basic_user_allowed_1_public_user_dod_filings',
3705+
'basic_user',
3706+
[BASIC_USER],
3707+
[{ 'date': datetime.utcnow() + datedelta(days=-3), 'user': 'public' }, { 'date': datetime.utcnow() + datedelta(days=-2), 'user': 'staff' }],
3708+
expected_lookup([FilingKey.AR_CORPS,
3709+
FilingKey.COA_CORPS,
3710+
FilingKey.COD_CORPS,
3711+
FilingKey.COO_CORPS,
3712+
FilingKey.DELAY_DISS,
3713+
FilingKey.TRANSITION,
3714+
FilingKey.TRANSPARENCY_REGISTER_ANNUAL,
3715+
FilingKey.TRANSPARENCY_REGISTER_CHANGE,
3716+
FilingKey.TRANSPARENCY_REGISTER_INITIAL])),
3717+
('basic_user_not_allowed_2_public_user_dod_filings',
3718+
'basic_user',
3719+
[BASIC_USER],
3720+
[{ 'date': datetime.utcnow() + datedelta(days=-3), 'user': 'public' }, { 'date': datetime.utcnow() + datedelta(days=-2), 'user': 'public' }],
3721+
expected_lookup([FilingKey.AR_CORPS,
3722+
FilingKey.COA_CORPS,
3723+
FilingKey.COD_CORPS,
3724+
FilingKey.COO_CORPS,
3725+
FilingKey.TRANSITION,
3726+
FilingKey.TRANSPARENCY_REGISTER_ANNUAL,
3727+
FilingKey.TRANSPARENCY_REGISTER_CHANGE,
3728+
FilingKey.TRANSPARENCY_REGISTER_INITIAL])),
3729+
('basic_user_allowed_1_current_1_prev',
3730+
'basic_user',
3731+
[BASIC_USER],
3732+
[{ 'date': datetime.utcnow() + datedelta(days=-3), 'user': 'public' }, { 'date': datetime.utcnow() + datedelta(days=-40), 'user': 'public' }],
3733+
expected_lookup([FilingKey.AR_CORPS,
3734+
FilingKey.COA_CORPS,
3735+
FilingKey.COD_CORPS,
3736+
FilingKey.COO_CORPS,
3737+
FilingKey.DELAY_DISS,
3738+
FilingKey.TRANSITION,
3739+
FilingKey.TRANSPARENCY_REGISTER_ANNUAL,
3740+
FilingKey.TRANSPARENCY_REGISTER_CHANGE,
3741+
FilingKey.TRANSPARENCY_REGISTER_INITIAL])),
3742+
]
3743+
)
3744+
def test_get_allowed_filings_blocker_max_delays(monkeypatch, app, session, jwt, test_name, username, roles, dods, expected):
3745+
"""Assert that get allowed returns valid filings when business is in dissolution."""
3746+
token = helper_create_jwt(jwt, roles=roles, username=username)
3747+
headers = {'Authorization': 'Bearer ' + token, 'Account-Id': 1}
3748+
3749+
def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods
3750+
return headers[one]
3751+
3752+
with app.test_request_context():
3753+
monkeypatch.setattr('flask.request.headers.get', mock_auth)
3754+
monkeypatch.setattr(
3755+
'legal_api.services.flags.value',
3756+
lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition"
3757+
if flag == 'enabled-specific-filings' else {}
3758+
)
3759+
monkeypatch.setattr(
3760+
'legal_api.models.User.get_or_create_user_by_jwt',
3761+
lambda _: None
3762+
)
3763+
3764+
identifier = 'BC7654321'
3765+
business = factory_business(identifier=identifier, entity_type=Business.LegalTypes.COMP)
3766+
delay_filing_json = {
3767+
'filing': {
3768+
'header': {
3769+
'name': 'dissolution'
3770+
},
3771+
'dissolution': {
3772+
'dissolutionType': 'delay',
3773+
'delayType': 'custom'
3774+
}
3775+
}
3776+
}
3777+
for delay in dods:
3778+
filing = factory_completed_filing(business, delay_filing_json, delay['date'], None, None, 'dissolution', 'delay')
3779+
if delay['user'] == 'staff':
3780+
filing.submitter_roles = 'staff'
3781+
filing.save()
3782+
batch = factory_batch()
3783+
batch_processing = factory_batch_processing(
3784+
batch_id=batch.id,
3785+
business_id=business.id,
3786+
identifier=identifier,
3787+
trigger_date=datetime.utcnow() + datedelta(days=10)
3788+
)
3789+
batch_processing.created_date = datetime.utcnow() + datedelta(days=-10)
3790+
batch_processing.save()
3791+
3792+
filing_types = get_allowed_filings(business, Business.State.ACTIVE, Business.LegalTypes.COMP, jwt)
3793+
assert filing_types == expected
3794+
3795+
36123796
def create_incomplete_filing(business,
36133797
filing_name,
36143798
filing_status,

0 commit comments

Comments
 (0)