diff --git a/legal-api/src/legal_api/resources/v1/business/business_filings.py b/legal-api/src/legal_api/resources/v1/business/business_filings.py index aa63a4cb09..0c10465faa 100644 --- a/legal-api/src/legal_api/resources/v1/business/business_filings.py +++ b/legal-api/src/legal_api/resources/v1/business/business_filings.py @@ -81,7 +81,7 @@ def get(identifier, filing_id=None): # pylint: disable=too-many-return-statemen if not rv.storage: return jsonify({'message': f'{identifier} no filings found'}), HTTPStatus.NOT_FOUND if str(request.accept_mimetypes) == 'application/pdf' and filing_id: - if rv.filing_type in ['amalgamationApplication', 'incorporationApplication']: + if rv.filing_type in ['amalgamationApplication', 'incorporationApplication', 'continuationIn']: return legal_api.reports.get_pdf(rv.storage, None) if original_filing: diff --git a/legal-api/src/legal_api/resources/v2/business/__init__.py b/legal-api/src/legal_api/resources/v2/business/__init__.py index 3abbe83b5b..a25986f34d 100644 --- a/legal-api/src/legal_api/resources/v2/business/__init__.py +++ b/legal-api/src/legal_api/resources/v2/business/__init__.py @@ -27,6 +27,14 @@ from .business_resolutions import get_resolutions from .business_share_classes import get_share_class from .business_tasks import get_tasks +from .colin_sync import ( + get_all_identifiers_without_tax_id, + get_colin_event_id, + get_completed_filings_for_colin, + set_tax_ids, + update_colin_event_id, + update_colin_id, +) from .filing_comments import get_filing_comments, not_allowed_filing_comments, post_filing_comments diff --git a/legal-api/src/legal_api/resources/v2/business/colin_sync.py b/legal-api/src/legal_api/resources/v2/business/colin_sync.py new file mode 100644 index 0000000000..a2b48a85a1 --- /dev/null +++ b/legal-api/src/legal_api/resources/v2/business/colin_sync.py @@ -0,0 +1,186 @@ +# Copyright © 2024 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Calls used by internal services jobs (update_colin_filings, update_legal_filings). + +These endpoint are reqired as long as we sync to colin. +""" +from http import HTTPStatus + +from flask import current_app, jsonify, request +from flask_cors import cross_origin + +from legal_api.exceptions import BusinessException +from legal_api.models import Business, Filing, UserRoles, db +from legal_api.models.colin_event_id import ColinEventId +from legal_api.services.business_details_version import VersionedBusinessDetailsService +from legal_api.utils.auth import jwt + +from .bp import bp + + +@bp.route('/internal/filings', methods=['GET']) +@bp.route('/internal/filings/', methods=['GET']) +@cross_origin(origin='*') +@jwt.has_one_of_roles([UserRoles.colin]) +def get_completed_filings_for_colin(status=None): + """Get filings by status formatted in json.""" + pending_filings = [] + filings = [] + + if status is None: + pending_filings = Filing.get_completed_filings_for_colin() + for filing in pending_filings: + filing_json = filing.filing_json + business = Business.find_by_internal_id(filing.business_id) + business_revision = VersionedBusinessDetailsService.get_business_revision_obj(filing.transaction_id, + business.id) + if filing_json and filing.filing_type != 'lear_epoch' and \ + (filing.filing_type != 'correction' or business.legal_type != Business.LegalTypes.COOP.value): + filing_json['filingId'] = filing.id + filing_json['filing']['header']['learEffectiveDate'] = filing.effective_date.isoformat() + if not filing_json['filing'].get('business'): + filing_json['filing']['business'] = VersionedBusinessDetailsService.business_revision_json( + business_revision, business.json()) + elif not filing_json['filing']['business'].get('legalName'): + filing_json['filing']['business']['legalName'] = business.legal_name + if filing.filing_type == 'correction': + colin_ids = \ + ColinEventId.get_by_filing_id(filing_json['filing']['correction']['correctedFilingId']) + if not colin_ids: + continue + filing_json['filing']['correction']['correctedFilingColinId'] = colin_ids[0] # should only be 1 + filings.append(filing_json) + return jsonify(filings), HTTPStatus.OK + + pending_filings = Filing.get_all_filings_by_status(status) + for filing in pending_filings: + filings.append(filing.json) + return jsonify(filings), HTTPStatus.OK + + +@bp.route('/internal/filings/', methods=['PATCH']) +@cross_origin(origin='*') +@jwt.has_one_of_roles([UserRoles.colin]) +def update_colin_id(filing_id): + """Patch the colin_event_id for a filing.""" + # check authorization + try: + json_input = request.get_json() + if not json_input: + return None, None, {'message': f'No filing json data in body of patch for {filing_id}.'}, \ + HTTPStatus.BAD_REQUEST + + colin_ids = json_input['colinIds'] + filing = Filing.find_by_id(filing_id) + if not filing: + return {'message': f'{filing_id} no filings found'}, HTTPStatus.NOT_FOUND + for colin_id in colin_ids: + try: + colin_event_id_obj = ColinEventId() + colin_event_id_obj.colin_event_id = colin_id + filing.colin_event_ids.append(colin_event_id_obj) + filing.save() + except BusinessException as err: + current_app.logger.Error(f'Error adding colin event id {colin_id} to filing with id {filing_id}') + return None, None, {'message': err.error}, err.status_code + + return jsonify(filing.json), HTTPStatus.ACCEPTED + except Exception as err: + current_app.logger.Error(f'Error patching colin event id for filing with id {filing_id}') + raise err + + +@bp.route('/internal/filings/colin_id', methods=['GET']) +@bp.route('/internal/filings/colin_id/', methods=['GET']) +@cross_origin(origin='*') +@jwt.has_one_of_roles([UserRoles.colin]) +def get_colin_event_id(colin_id=None): + """Get the last colin id updated in legal.""" + try: + if colin_id: + colin_id_obj = ColinEventId.get_by_colin_id(colin_id) + if not colin_id_obj: + return {'message': 'No colin ids found'}, HTTPStatus.NOT_FOUND + return {'colinId': colin_id_obj.colin_event_id}, HTTPStatus.OK + except Exception as err: + current_app.logger.Error(f'Failed to get last updated colin event id: {err}') + raise err + + query = db.session.execute( + """ + select last_event_id from colin_last_update + order by id desc + """ + ) + last_event_id = query.fetchone() + if not last_event_id or not last_event_id[0]: + return {'message': 'No colin ids found'}, HTTPStatus.NOT_FOUND + + return {'maxId': last_event_id[0]}, HTTPStatus.OK if request.method == 'GET' else HTTPStatus.CREATED + + +@bp.route('/internal/filings/colin_id/', methods=['POST']) +@cross_origin(origin='*') +@jwt.has_one_of_roles([UserRoles.colin]) +def update_colin_event_id(colin_id): + """Add a row to the colin_last_update table.""" + try: + db.session.execute( + f""" + insert into colin_last_update (last_update, last_event_id) + values (current_timestamp, {colin_id}) + """ + ) + db.session.commit() + return get_colin_event_id() + + except Exception as err: # pylint: disable=broad-except + current_app.logger.error(f'Error updating colin_last_update table in legal db: {err}') + return {'message: failed to update colin_last_update.', 500} + + +@bp.route('/internal/tax_ids', methods=['GET']) +@cross_origin(origin='*') +@jwt.has_one_of_roles([UserRoles.colin]) +def get_all_identifiers_without_tax_id(): + """Return all identifiers with no tax_id set that are supposed to have a tax_id. + + Excludes COOPS because they do not get a tax id/business number. + Excludes SP/GP we don't sync firms to colin, we use entity-bn to get tax id/business number from CRA. + """ + identifiers = [] + bussinesses_no_taxid = Business.get_all_by_no_tax_id() + for business in bussinesses_no_taxid: + identifiers.append(business.identifier) + return jsonify({'identifiers': identifiers}), HTTPStatus.OK + + +@bp.route('/internal/tax_ids', methods=['POST']) +@cross_origin(origin='*') +@jwt.has_one_of_roles([UserRoles.colin]) +def set_tax_ids(): + """Set tax ids for businesses for given identifiers.""" + json_input = request.get_json() + if not json_input: + return ({'message': 'No identifiers in body of post.'}, HTTPStatus.BAD_REQUEST) + + for identifier in json_input.keys(): + # json input is a dict -> identifier: tax id + business = Business.find_by_identifier(identifier) + if business: + business.tax_id = json_input[identifier] + business.save() + else: + current_app.logger.error(f'Unable to update tax_id for business ({identifier}), which is missing in lear') + return jsonify({'message': 'Successfully updated tax ids.'}), HTTPStatus.CREATED diff --git a/legal-api/tests/unit/resources/v2/test_colin_sync.py b/legal-api/tests/unit/resources/v2/test_colin_sync.py new file mode 100644 index 0000000000..91a24c1437 --- /dev/null +++ b/legal-api/tests/unit/resources/v2/test_colin_sync.py @@ -0,0 +1,212 @@ +# Copyright © 2024 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests to assure the colin sync end-point.""" +import copy +from datetime import datetime +from http import HTTPStatus + +import datedelta +import pytest +from registry_schemas.example_data import ( + ANNUAL_REPORT, + CHANGE_OF_ADDRESS, + CORRECTION_AR, + CORRECTION_INCORPORATION, + FILING_HEADER, + INCORPORATION_FILING_TEMPLATE, +) + +from legal_api.models import Business, Filing +from legal_api.services.authz import COLIN_SVC_ROLE +from tests.unit.services.utils import create_header +from tests.unit.models import factory_business_mailing_address, factory_business, factory_completed_filing, factory_filing # noqa:E501,I001 + + +def test_get_internal_filings(session, client, jwt): + """Assert that the internal filings get endpoint returns all completed filings without colin ids.""" + from legal_api.models.colin_event_id import ColinEventId + from tests.unit.models import factory_error_filing, factory_pending_filing + # setup + identifier = 'CP7654321' + b = factory_business(identifier) + factory_business_mailing_address(b) + + filing1 = factory_completed_filing(b, ANNUAL_REPORT) + filing2 = factory_completed_filing(b, ANNUAL_REPORT) + filing3 = factory_pending_filing(b, ANNUAL_REPORT) + filing4 = factory_filing(b, ANNUAL_REPORT) + filing5 = factory_error_filing(b, ANNUAL_REPORT) + filing6 = factory_completed_filing(b, CORRECTION_AR) + + assert filing1.status == Filing.Status.COMPLETED.value + # completed with colin_event_id + print(filing2.colin_event_ids) + assert len(filing2.colin_event_ids) == 0 + colin_event_id = ColinEventId() + colin_event_id.colin_event_id = 12345 + filing2.colin_event_ids.append(colin_event_id) + filing2.save() + assert filing2.status == Filing.Status.COMPLETED.value + assert filing2.colin_event_ids + # pending with no colin_event_ids + assert filing3.status == Filing.Status.PENDING.value + # draft with no colin_event_ids + assert filing4.status == Filing.Status.DRAFT.value + # error with no colin_event_ids + assert filing5.status == Filing.Status.PAID.value + # completed correction with no colin_event_ids + assert filing6.status == Filing.Status.COMPLETED.value + + # test endpoint returned filing1 only (completed, no corrections, with no colin id set) + rv = client.get('/api/v2/businesses/internal/filings', + headers=create_header(jwt, [COLIN_SVC_ROLE])) + assert rv.status_code == HTTPStatus.OK + assert len(rv.json) == 1 + assert rv.json[0]['filingId'] == filing1.id + + +@pytest.mark.parametrize('identifier, base_filing, corrected_filing, colin_id', [ + ('BC1234567', CORRECTION_INCORPORATION, INCORPORATION_FILING_TEMPLATE, 1234), + ('BC1234568', CORRECTION_INCORPORATION, INCORPORATION_FILING_TEMPLATE, None), +]) +def test_get_bcomp_corrections(session, client, jwt, identifier, base_filing, corrected_filing, colin_id): + """Assert that the internal filings get endpoint returns corrections for bcomps.""" + # setup + b = factory_business(identifier=identifier, entity_type=Business.LegalTypes.BCOMP.value) + factory_business_mailing_address(b) + + incorp_filing = factory_completed_filing(business=b, data_dict=corrected_filing, colin_id=colin_id) + correction_filing = copy.deepcopy(base_filing) + correction_filing['filing']['correction']['correctedFilingId'] = incorp_filing.id + filing = factory_completed_filing(b, correction_filing) + + # test endpoint returns filing + rv = client.get('/api/v2/businesses/internal/filings', + headers=create_header(jwt, [COLIN_SVC_ROLE])) + assert rv.status_code == HTTPStatus.OK + assert len(rv.json) == 1 + if colin_id: + assert rv.json[0]['filingId'] == filing.id + else: + assert rv.json[0]['filingId'] == incorp_filing.id + + +def test_patch_internal_filings(session, client, jwt): + """Assert that the internal filings patch endpoint updates the colin_event_id.""" + from legal_api.models.colin_event_id import ColinEventId + # setup + identifier = 'CP7654321' + b = factory_business(identifier) + factory_business_mailing_address(b) + filing = factory_completed_filing(b, ANNUAL_REPORT) + colin_id = 1234 + + # make request + rv = client.patch(f'/api/v2/businesses/internal/filings/{filing.id}', + json={'colinIds': [colin_id]}, + headers=create_header(jwt, [COLIN_SVC_ROLE]) + ) + + # test result + assert rv.status_code == HTTPStatus.ACCEPTED + filing = Filing.find_by_id(filing.id) + assert colin_id in ColinEventId.get_by_filing_id(filing.id) + assert rv.json['filing']['header']['filingId'] == filing.id + assert colin_id in rv.json['filing']['header']['colinIds'] + + +def test_get_colin_id(session, client, jwt): + """Assert the internal/filings/colin_id get endpoint returns properly.""" + from legal_api.models.colin_event_id import ColinEventId + # setup + identifier = 'CP7654321' + b = factory_business(identifier) + factory_business_mailing_address(b) + filing = factory_completed_filing(b, ANNUAL_REPORT) + colin_event_id = ColinEventId() + colin_event_id.colin_event_id = 1234 + filing.colin_event_ids.append(colin_event_id) + filing.save() + + rv = client.get(f'/api/v2/businesses/internal/filings/colin_id/{colin_event_id.colin_event_id}', + headers=create_header(jwt, [COLIN_SVC_ROLE])) + assert rv.status_code == HTTPStatus.OK + assert rv.json == {'colinId': colin_event_id.colin_event_id} + + rv = client.get(f'/api/v2/businesses/internal/filings/colin_id/{1}', + headers=create_header(jwt, [COLIN_SVC_ROLE])) + assert rv.status_code == HTTPStatus.NOT_FOUND + + +def test_get_colin_last_update(session, client, jwt): + """Assert the get endpoint for ColinLastUpdate returns last updated colin id.""" + from tests.unit.models import db + # setup + colin_id = 1234 + db.session.execute( + f""" + insert into colin_last_update (last_update, last_event_id) + values (current_timestamp, {colin_id}) + """ + ) + + rv = client.get('/api/v2/businesses/internal/filings/colin_id', + headers=create_header(jwt, [COLIN_SVC_ROLE])) + assert rv.status_code == HTTPStatus.OK + assert rv.json == {'maxId': colin_id} + + +def test_post_colin_last_update(session, client, jwt): + """Assert the internal/filings/colin_id post endpoint updates the colin_last_update table.""" + colin_id = 1234 + rv = client.post(f'/api/v2/businesses/internal/filings/colin_id/{colin_id}', + headers=create_header(jwt, [COLIN_SVC_ROLE]) + ) + assert rv.status_code == HTTPStatus.CREATED + assert rv.json == {'maxId': colin_id} + + +def test_future_filing_coa(session, client, jwt): + """Assert that future effective filings are saved and have the correct status changes.""" + import pytz + from tests.unit.models import factory_pending_filing + # setup + identifier = 'CP7654321' + b = factory_business(identifier, (datetime.utcnow() - datedelta.YEAR), None, Business.LegalTypes.BCOMP.value) + factory_business_mailing_address(b) + coa = copy.deepcopy(FILING_HEADER) + coa['filing']['header']['name'] = 'changeOfAddress' + coa['filing']['changeOfAddress'] = CHANGE_OF_ADDRESS + coa['filing']['changeOfAddress']['offices']['registeredOffice']['deliveryAddress']['addressCountry'] = 'CA' + coa['filing']['changeOfAddress']['offices']['registeredOffice']['mailingAddress']['addressCountry'] = 'CA' + coa['filing']['business']['identifier'] = identifier + + filing = factory_pending_filing(b, coa) + filing.effective_date = datetime.utcnow() + datedelta.DAY + filing.save() + assert filing.status == Filing.Status.PENDING.value + + filing.payment_completion_date = pytz.utc.localize(datetime.utcnow()) + filing.save() + + assert filing.status == Filing.Status.PAID.value + + rv = client.get('/api/v2/businesses/internal/filings/PAID', headers=create_header(jwt, [COLIN_SVC_ROLE])) + paid_filings = rv.json + assert paid_filings[0] + # check values that future effective filings job depends on are there + assert paid_filings[0]['filing']['header']['filingId'] == filing.id + assert paid_filings[0]['filing']['header']['paymentToken'] + assert paid_filings[0]['filing']['header']['effectiveDate']