diff --git a/legal-api/src/legal_api/services/authz.py b/legal-api/src/legal_api/services/authz.py index d3a5a5069..a5967dda1 100644 --- a/legal-api/src/legal_api/services/authz.py +++ b/legal-api/src/legal_api/services/authz.py @@ -52,6 +52,7 @@ class BusinessBlocker(str, Enum): NOT_IN_GOOD_STANDING = 'NOT_IN_GOOD_STANDING' AMALGAMATING_BUSINESS = 'AMALGAMATING_BUSINESS' IN_DISSOLUTION = 'IN_DISSOLUTION' + FILING_WITHDRAWAL = 'FILING_WITHDRAWAL' class BusinessRequirement(str, Enum): @@ -314,6 +315,12 @@ def get_allowable_filings_dict(): 'business': [BusinessBlocker.DEFAULT] } } + }, + 'noticeOfWithdrawal': { + 'legalTypes': ['BC', 'BEN', 'CC', 'ULC', 'C', 'CBEN', 'CUL', 'CCC'], + 'blockerChecks': { + 'business': [BusinessBlocker.FILING_WITHDRAWAL] + } } }, Business.State.HISTORICAL: { @@ -648,7 +655,8 @@ def business_blocker_check(business: Business, is_ignore_draft_blockers: bool = BusinessBlocker.DRAFT_PENDING: False, BusinessBlocker.NOT_IN_GOOD_STANDING: False, BusinessBlocker.AMALGAMATING_BUSINESS: False, - BusinessBlocker.IN_DISSOLUTION: False + BusinessBlocker.IN_DISSOLUTION: False, + BusinessBlocker.FILING_WITHDRAWAL: False } if not business: @@ -671,6 +679,9 @@ def business_blocker_check(business: Business, is_ignore_draft_blockers: bool = if business.in_dissolution: business_blocker_checks[BusinessBlocker.IN_DISSOLUTION] = True + if has_notice_of_withdrawal_filing_blocker(business): + business_blocker_checks[BusinessBlocker.FILING_WITHDRAWAL] = True + return business_blocker_checks @@ -791,6 +802,24 @@ def has_blocker_warning_filing(warnings: List, blocker_checks: dict): return warning_matches +def has_notice_of_withdrawal_filing_blocker(business: Business): + """Check if there are any blockers specific to Notice of Withdrawal.""" + if business.admin_freeze: + return True + + filing_statuses = [Filing.Status.DRAFT.value, + Filing.Status.PENDING.value, + Filing.Status.PENDING_CORRECTION.value, + Filing.Status.ERROR.value] + blocker_filing_matches = Filing.get_filings_by_status(business.id, filing_statuses) + if any(blocker_filing_matches): + return True + + now = datetime.now(timezone.utc) + paid_filings = Filing.get_filings_by_status(business.id, [Filing.Status.PAID.value]) + return not any(f.effective_date and f.effective_date > now for f in paid_filings) + + def get_allowed(state: Business.State, legal_type: str, jwt: JwtManager): """Get allowed type of filing types for the current user.""" user_role = 'general' diff --git a/legal-api/tests/unit/services/test_authorization.py b/legal-api/tests/unit/services/test_authorization.py index 904e7fb1d..bfe45e518 100644 --- a/legal-api/tests/unit/services/test_authorization.py +++ b/legal-api/tests/unit/services/test_authorization.py @@ -146,6 +146,7 @@ class FilingKey(str, Enum): AMALGAMATION_REGULAR = 'AMALGAMATION_REGULAR' AMALGAMATION_VERTICAL = 'AMALGAMATION_VERTICAL' AMALGAMATION_HORIZONTAL = 'AMALGAMATION_HORIZONTAL' + NOTICE_OF_WITHDRAWAL = 'NOTICE_OF_WITHDRAWAL' EXPECTED_DATA = { @@ -219,7 +220,8 @@ class FilingKey(str, Enum): FilingKey.PUT_BACK_ON: {'displayName': 'Correction - Put Back On', 'feeCode': 'NOFEE', 'name': 'putBackOn'}, FilingKey.AMALGAMATION_REGULAR: {'name': 'amalgamationApplication', 'type': 'regular', 'displayName': 'Amalgamation Application (Regular)', 'feeCode': 'AMALR'}, FilingKey.AMALGAMATION_VERTICAL: {'name': 'amalgamationApplication', 'type': 'vertical', 'displayName': 'Amalgamation Application Short-form (Vertical)', 'feeCode': 'AMALV'}, - FilingKey.AMALGAMATION_HORIZONTAL: {'name': 'amalgamationApplication', 'type': 'horizontal', 'displayName': 'Amalgamation Application Short-form (Horizontal)', 'feeCode': 'AMALH'} + FilingKey.AMALGAMATION_HORIZONTAL: {'name': 'amalgamationApplication', 'type': 'horizontal', 'displayName': 'Amalgamation Application Short-form (Horizontal)', 'feeCode': 'AMALH'}, + FilingKey.NOTICE_OF_WITHDRAWAL: {'displayName': 'Notice of Withdrawal', 'feeCode': 'NWITH', 'name': 'noticeOfWithdrawal'} } EXPECTED_DATA_CONT_IN = { @@ -297,7 +299,9 @@ class FilingKey(str, Enum): 'displayName': 'Amalgamation Application Short-form (Vertical)', 'feeCode': None}, FilingKey.AMALGAMATION_HORIZONTAL: {'name': 'amalgamationApplication', 'type': 'horizontal', 'displayName': 'Amalgamation Application Short-form (Horizontal)', - 'feeCode': None} + 'feeCode': None}, + FilingKey.NOTICE_OF_WITHDRAWAL: {'displayName': 'Notice of Withdrawal', 'feeCode': 'NWITH', + 'name': 'noticeOfWithdrawal'} } BLOCKER_FILING_STATUSES = factory_incomplete_statuses() @@ -527,13 +531,14 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me 'changeOfDirectors', 'consentContinuationOut', 'continuationOut', 'correction', 'courtOrder', {'dissolution': ['voluntary', 'administrative']}, 'incorporationApplication', 'registrarsNotation', 'registrarsOrder', 'transition', - {'restoration': ['limitedRestorationExtension', 'limitedRestorationToFull']}]), + {'restoration': ['limitedRestorationExtension', 'limitedRestorationToFull']}, 'noticeOfWithdrawal']), ('staff_active_continue_in_corps', Business.State.ACTIVE, ['C', 'CBEN', 'CUL', 'CCC'], 'staff', [STAFF_ROLE], ['adminFreeze', 'agmExtension', 'agmLocationChange', 'alteration', {'amalgamationApplication': ['regular', 'vertical', 'horizontal']}, 'annualReport', 'changeOfAddress', 'changeOfDirectors', 'continuationIn', 'consentContinuationOut', 'continuationOut', 'correction', 'courtOrder', {'dissolution': ['voluntary', 'administrative']}, 'registrarsNotation', 'registrarsOrder', - 'transition', {'restoration': ['limitedRestorationExtension', 'limitedRestorationToFull']}]), + 'transition', {'restoration': ['limitedRestorationExtension', 'limitedRestorationToFull']}, + 'noticeOfWithdrawal']), ('staff_active_llc', Business.State.ACTIVE, ['LLC'], 'staff', [STAFF_ROLE], []), ('staff_active_firms', Business.State.ACTIVE, ['SP', 'GP'], 'staff', [STAFF_ROLE], ['adminFreeze', 'changeOfRegistration', 'conversion', 'correction', 'courtOrder', @@ -1938,7 +1943,13 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me filing_sub_type=filing_sub_type, is_future_effective=is_fed) allowed_filing_types = get_allowed_filings(business, state, legal_type, jwt) - assert allowed_filing_types == expected + + current_expected = expected.copy() + if username == 'staff' and filing_status == Filing.Status.PAID.value: + notice_of_withdrawal = expected_lookup([FilingKey.NOTICE_OF_WITHDRAWAL])[0] + if notice_of_withdrawal not in current_expected: + current_expected.append(notice_of_withdrawal) + assert allowed_filing_types == current_expected @pytest.mark.parametrize( @@ -2674,6 +2685,172 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me filing_types = get_allowed_filings(business, state, legal_type, jwt) assert filing_types == expected +@pytest.mark.parametrize( + 'test_name,state,legal_types,username,roles,blocker_status,expected', + [ + # active business - staff user + ('staff_active_cp', Business.State.ACTIVE, ['CP'], 'staff', [STAFF_ROLE], None, + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.AR_CP, + FilingKey.COA_CP, + FilingKey.COD_CP, + FilingKey.CORRCTN, + FilingKey.COURT_ORDER, + FilingKey.ADM_DISS, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.SPECIAL_RESOLUTION])), + ('staff_active_corps', + Business.State.ACTIVE, ['BC', 'BEN', 'CC', 'ULC'], 'staff', [STAFF_ROLE], None, + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.ALTERATION, + FilingKey.AR_CORPS, + FilingKey.COA_CORPS, + FilingKey.COD_CORPS, + FilingKey.CORRCTN, + FilingKey.COURT_ORDER, + FilingKey.ADM_DISS, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.TRANSITION, + ])), + ('staff_active_corps_with_FED', + Business.State.ACTIVE, ['BC', 'BEN', 'CC', 'ULC'], 'staff', [STAFF_ROLE], 'FUTURE_EFFECTIVE', + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.COURT_ORDER, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.TRANSITION, + FilingKey.NOTICE_OF_WITHDRAWAL + ])), + ('staff_active_corps_business_frozen', + Business.State.ACTIVE, ['BC', 'BEN', 'CC', 'ULC'], 'staff', [STAFF_ROLE], 'FROZEN', + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.COURT_ORDER, + FilingKey.ADM_DISS, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.TRANSITION, + ])), + ('staff_active_corps_with_draft_filing', + Business.State.ACTIVE, ['BC', 'BEN', 'CC', 'ULC'], 'staff', [STAFF_ROLE], 'DRAFT', + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.COURT_ORDER, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.TRANSITION + ])), + ('staff_active_continue_in_corps', Business.State.ACTIVE, ['C', 'CBEN', 'CCC', 'CUL'], 'staff', [STAFF_ROLE], + None, + expected_lookup([ + FilingKey.ADMN_FRZE, + FilingKey.ALTERATION, + FilingKey.AR_CORPS, + FilingKey.COA_CORPS, + FilingKey.COD_CORPS, + FilingKey.CORRCTN, + FilingKey.COURT_ORDER, + FilingKey.ADM_DISS, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.TRANSITION])), + ('staff_active_llc', Business.State.ACTIVE, ['LLC'], 'staff', [STAFF_ROLE], None, []), + ('staff_active_firms', Business.State.ACTIVE, ['SP', 'GP'], 'staff', [STAFF_ROLE], None, + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.CHANGE_OF_REGISTRATION, + FilingKey.CONV_FIRMS, + FilingKey.CORRCTN_FIRMS, + FilingKey.COURT_ORDER, + FilingKey.VOL_DISS_FIRMS, + FilingKey.ADM_DISS_FIRMS, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER])), + + # active business - general user + ('general_user_cp', Business.State.ACTIVE, ['CP'], 'general', [BASIC_USER], None, + expected_lookup([FilingKey.AR_CP, + FilingKey.COA_CP, + FilingKey.COD_CP, + FilingKey.SPECIAL_RESOLUTION])), + ('general_user_corps', Business.State.ACTIVE, ['BC', 'BEN', 'CC', 'ULC'], 'general', [BASIC_USER], None, + expected_lookup([ + FilingKey.ALTERATION, + FilingKey.AR_CORPS, + FilingKey.COA_CORPS, + FilingKey.COD_CORPS, + FilingKey.TRANSITION])), + ('general_user_continue_in_corps', Business.State.ACTIVE, ['C', 'CBEN', 'CCC', 'CUL'], 'general', [BASIC_USER], + None, + expected_lookup([FilingKey.ALTERATION, + FilingKey.AR_CORPS, + FilingKey.COA_CORPS, + FilingKey.COD_CORPS, + FilingKey.TRANSITION])), + ('general_user_llc', Business.State.ACTIVE, ['LLC'], 'general', [BASIC_USER], None, []), + ('general_user_firms', Business.State.ACTIVE, ['SP', 'GP'], 'general', [BASIC_USER], None, + expected_lookup([FilingKey.CHANGE_OF_REGISTRATION, + FilingKey.VOL_DISS_FIRMS])), + + # historical business - staff user + ('staff_historical_corps', Business.State.HISTORICAL, ['BC', 'BEN', 'CC', 'ULC'], 'staff', [STAFF_ROLE], None, + expected_lookup([FilingKey.COURT_ORDER, + FilingKey.PUT_BACK_ON, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.RESTRN_FULL_CORPS, + FilingKey.RESTRN_LTD_CORPS])), + ('staff_historical_continue_in_corps', Business.State.HISTORICAL, ['C', 'CBEN', 'CCC', 'CUL'], 'staff', + [STAFF_ROLE], None, + expected_lookup([FilingKey.COURT_ORDER, + FilingKey.PUT_BACK_ON, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.RESTRN_FULL_CORPS, + FilingKey.RESTRN_LTD_CORPS])), + + # historical business - general user + ('general_user_historical_corps', Business.State.HISTORICAL, ['BC', 'BEN', 'CC', 'ULC'], 'general', + [BASIC_USER], None, []), + ('general_user_historical_continue_in_corps', Business.State.HISTORICAL, ['C', 'CBEN', 'CCC', 'CUL'], + 'general', [BASIC_USER], None, []), + ] +) +def test_allowed_filings_notice_of_withdrawal(monkeypatch, app, session, jwt, test_name, state, legal_types, username, + roles, blocker_status, expected): + """Assert that get allowed returns valid filings for notice of withdrawal.""" + token = helper_create_jwt(jwt, roles=roles, username=username) + headers = {'Authorization': 'Bearer ' + token} + + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] + + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + + for legal_type in legal_types: + identifier = (f'BC{random.SystemRandom().getrandbits(0x58)}')[:9] + business = factory_business(identifier=identifier, + entity_type=legal_type, + state=state) + + if blocker_status == 'FROZEN': + business = factory_business(identifier=identifier, + entity_type=legal_type, + state=state, + admin_freeze=True) + elif blocker_status == 'DRAFT': + create_incomplete_filing(business=business, + filing_name='unknownFiling', + filing_status='DRAFT') + elif blocker_status == 'FUTURE_EFFECTIVE': + create_incomplete_filing(business=business, + filing_name='unknownFiling', + filing_status='PAID', + is_future_effective=True) + + allowed_filing_types = get_allowed_filings(business, state, legal_type, jwt) + assert allowed_filing_types == expected + @patch('legal_api.models.User.find_by_jwt_token', return_value=User(id=1, login_source='BCSC')) @patch('legal_api.services.authz.is_self_registered_owner_operator', return_value=True)