def validate(business: Business, incorporation_json: Dict): """Validate the Incorporation filing.""" if not business or not incorporation_json: return Error( HTTPStatus.BAD_REQUEST, [{ 'error': babel('A valid business and filing are required.') }]) msg = [] # What are the rules for saving an incorporation temp_identifier = incorporation_json['filing']['incorporationApplication'][ 'nameRequest']['nrNumber'] if business.identifier != temp_identifier: msg.append({ 'error': babel( 'Business Identifier does not match the identifier in filing.') }) err = validate_offices(incorporation_json) if err: msg.append(err) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) return None
def validate(business: Business, registrars_order: Dict) -> Optional[Error]: """Validate the Registrars Order filing.""" if not business or not registrars_order: return Error( HTTPStatus.BAD_REQUEST, [{ 'error': babel('A valid business and filing are required.') }]) msg = [] effect_of_order = get_str(registrars_order, '/filing/registrarsOrder/effectOfOrder') if effect_of_order: if effect_of_order == 'planOfArrangement': file_number = get_str(registrars_order, '/filing/registrarsOrder/fileNumber') if not file_number: msg.append({ 'error': babel( 'Court Order Number is required when this filing is pursuant to a Plan of Arrangement.' ), 'path': '/filing/registrarsOrder/fileNumber' }) else: msg.append({ 'error': babel('Invalid effectOfOrder.'), 'path': '/filing/registrarsOrder/effectOfOrder' }) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) return None
def validate_pdf(file_key: str): """Validate the PDF file.""" msg = [] try: file = MinioService.get_file(file_key) open_pdf_file = io.BytesIO(file.data) pdf_reader = PyPDF2.PdfFileReader(open_pdf_file) pdf_size_units = pdf_reader.getPage(0).mediaBox if pdf_size_units.getWidth() != 612 or pdf_size_units.getHeight( ) != 792: msg.append({ 'error': babel( 'Document must be set to fit onto 8.5” x 11” letter-size paper.' ) }) file_info = MinioService.get_file_info(file_key) if file_info.size > 10000000: msg.append({'error': babel('File exceeds maximum size.')}) if pdf_reader.isEncrypted: msg.append({'error': babel('File must be unencrypted.')}) except Exception: msg.append({'error': babel('Invalid file.')}) if msg: return msg return None
def validate_correction_name_request(filing: Dict, corrected_filing: Dict) -> Optional[List]: """Validate correction of Name Request.""" nr_path = '/filing/incorporationApplication/nameRequest/nrNumber' nr_number = get_str(corrected_filing.json, nr_path) new_nr_number = get_str(filing, nr_path) # original filing has no nrNumber and new filing has nr Number (numbered -> named correction) # original filing nrNumber != new filing nrNumber (change of name using NR) msg = [] if nr_number == new_nr_number: return None # ensure NR is approved or conditionally approved nr_response = namex.query_nr_number(new_nr_number) validation_result = namex.validate_nr(nr_response.json()) if not validation_result['is_approved']: msg.append({'error': babel('Correction of Name Request is not approved.'), 'path': nr_path}) # ensure business type is BCOMP path = '/filing/incorporationApplication/nameRequest/legalType' legal_type = get_str(filing, path) if legal_type != Business.LegalTypes.BCOMP.value: msg.append({'error': babel('Correction of Name Request is not vaild for this type.'), 'path': path}) # ensure NR request has the same legal name path = '/filing/incorporationApplication/nameRequest/legalName' legal_name = get_str(filing, path) nr_name = namex.get_approved_name(nr_response.json()) if nr_name != legal_name: msg.append({'error': babel('Correction of Name Request has a different legal name.'), 'path': path}) if msg: return msg return None
def validate_incorporation_effective_date(incorporation_json) -> Error: """Return an error or warning message based on the effective date validation rules. Rules: - The effective date must be the correct format. - The effective date must be a minimum of 2 minutes in the future. - The effective date must be a maximum of 10 days in the future. """ # Setup msg = [] now = dt.utcnow() now_plus_2_minutes = now + timedelta(minutes=2) now_plus_10_days = now + timedelta(days=10) try: filing_effective_date = incorporation_json['filing']['header']['effectiveDate'] except KeyError: return msg try: effective_date = dt.fromisoformat(filing_effective_date) except ValueError: msg.append({'error': babel('%s is an invalid ISO format for effective_date.') % filing_effective_date}) return msg if effective_date < now_plus_2_minutes: msg.append({'error': babel('Invalid Datetime, effective date must be a minimum of 2 minutes ahead.')}) if effective_date > now_plus_10_days: msg.append({'error': babel('Invalid Datetime, effective date must be a maximum of 10 days ahead.')}) if msg: return msg return None
def validate(business: Business, court_order: Dict) -> Optional[Error]: """Validate the Court Order filing.""" if not business or not court_order: return Error( HTTPStatus.BAD_REQUEST, [{ 'error': babel('A valid business and filing are required.') }]) msg = [] if not get_str(court_order, '/filing/courtOrder/orderDetails'): msg.append({ 'error': babel('Court Order is required.'), 'path': '/filing/courtOrder/orderDetails' }) effect_of_order = get_str(court_order, '/filing/courtOrder/effectOfOrder') if effect_of_order and effect_of_order != 'planOfArrangement': msg.append({ 'error': babel('Invalid effectOfOrder.'), 'path': '/filing/courtOrder/effectOfOrder' }) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) return None
def validate_effective_date(business: Business, cod: Dict) -> List: """Return an error or warning message based on the effective date validation rules. Rules: - The effective date of change cannot be in the future. - The effective date cannot be a date prior to their Incorporation Date - The effective date of change cannot be a date that is farther in the past as a previous COD filing (Standalone or AR). - The effective date can be the same effective date as another COD filing (standalone OR AR). If this is the case: - COD filing that was filed most recently as the most current director information. """ try: filing_effective_date = cod['filing']['header']['effectiveDate'] except KeyError: try: # we'll assume the filing is at 0 hours UTC filing_effective_date = cod['filing']['header'][ 'date'] + 'T00:00:00+00:00' except KeyError: return { 'error': babel('No effective_date or filing date provided.') } try: effective_date = datetime.fromisoformat(filing_effective_date) except ValueError: return { 'error': babel('Invalid ISO format for effective_date or filing date.') } msg = [] # The effective date of change cannot be in the future if effective_date > datetime.utcnow(): msg.append( {'error': babel('Filing cannot have a future effective date.')}) # The effective date cannot be a date prior to their Incorporation Date if effective_date < business.founding_date: msg.append({ 'error': babel('Filing cannot be before a businesses founding date.') }) last_cod_filing = Filing.get_most_recent_legal_filing( business.id, Filing.FILINGS['changeOfDirectors']['name']) if last_cod_filing: if effective_date < last_cod_filing.effective_date: msg.append({ 'error': babel( "Filing's effective date cannot be before another Change of Director filing." ) }) return msg
def validate(business: Business, filing: Dict) -> Error: # pylint: disable=too-many-branches """Validate the Alteration filing.""" if not business or not filing: return Error(HTTPStatus.BAD_REQUEST, [{'error': babel('A valid business and filing are required.')}]) msg = [] msg.extend(company_name_validation(filing)) msg.extend(share_structure_validation(filing)) msg.extend(court_order_validation(filing)) msg.extend(type_change_validation(filing)) if err := has_at_least_one_share_class(filing, 'alteration'): msg.append({'error': babel(err), 'path': '/filing/alteration/shareStructure'})
def set_corp_type(business: Business, business_info: Dict) -> Dict: """Set the legal type of the business.""" if not business: return {'error': babel('Business required before type can be set.')} try: legal_type = business_info.get('legalType') if legal_type: business.legal_type = legal_type except (IndexError, KeyError, TypeError): return {'error': babel('A valid legal type must be provided.')} return None
def validate(incorporation_json: Dict): """Validate the Incorporation filing.""" if not incorporation_json: return Error(HTTPStatus.BAD_REQUEST, [{'error': babel('A valid filing is required.')}]) msg = [] err = validate_offices(incorporation_json) if err: msg.append(err) err = validate_roles(incorporation_json) if err: msg.append(err) err = validate_parties_mailing_address(incorporation_json) if err: msg.append(err) err = validate_share_structure(incorporation_json) if err: msg.append(err) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) return None
def validate_directors_addresses(cod: Dict) -> List: """Return an error message if the directors address are invalid. Address must contain a valid ISO-2 valid country. """ msg = [] directors = cod['filing']['changeOfDirectors']['directors'] for idx, director in enumerate(directors): # pylint: disable=too-many-nested-blocks; # noqa: E501 review this when implementing corrections for address_type in Address.JSON_ADDRESS_TYPES: if address_type in director: try: country = get_str(director, f'/{address_type}/addressCountry') _ = pycountry.countries.search_fuzzy(country)[0].alpha_2 except LookupError: msg.append({ 'error': babel( 'Address Country must resolve to a valid ISO-2 country.' ), 'path': f'/filing/changeOfDirectors/directors/{idx}/{address_type}/addressCountry' }) return msg
def get_businesses(identifier: str): """Return a JSON object with meta information about the Service.""" # check authorization # if not authorized(identifier, jwt, action=['view']): # return jsonify({'message': # f'You are not authorized to view business {identifier}.'}), \ # HTTPStatus.UNAUTHORIZED if identifier.startswith('T'): return {'message': babel('No information on temp registrations.')}, 200 business = Business.find_by_identifier(identifier) if not business: return jsonify({'message': f'{identifier} not found'}), HTTPStatus.NOT_FOUND business_json = business.json() recent_filing_json = CoreFiling.get_most_recent_filing_json( business.id, None, jwt) if recent_filing_json: business_json['submitter'] = recent_filing_json['filing']['header'][ 'submitter'] business_json['lastModified'] = recent_filing_json['filing']['header'][ 'date'] return jsonify(business=business_json)
def get_error_message(error_code: ErrorCode, **kwargs) -> Optional[str]: """Get a localized, formatted error message using the templates in the ERROR_MESSAGES dict.""" if template := ERROR_MESSAGES.get(error_code, None): fmt = MissingKeysFormatter() template = babel(template) msg = fmt.format(template, **kwargs) return msg
def validate(incorporation_json: Dict): """Validate the Incorporation filing.""" if not incorporation_json: return Error(HTTPStatus.BAD_REQUEST, [{ 'error': babel('A valid filing is required.') }]) msg = [] err = validate_offices(incorporation_json) if err: msg.extend(err) err = validate_roles(incorporation_json) if err: msg.extend(err) err = validate_parties_mailing_address(incorporation_json) if err: msg.extend(err) err = validate_share_structure( incorporation_json, coreFiling.FilingTypes.INCORPORATIONAPPLICATION.value) if err: msg.extend(err) err = validate_incorporation_effective_date(incorporation_json) if err: msg.extend(err) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) return None
def validate_effective_date(business: Business, cod: Dict) -> List: """Return an error or warning message based on the effective date validation rules. Rules: (text from the BA rules document) - The effective date of change cannot be in the future. - The effective date cannot be a date prior to their Incorporation Date. - The effective date of change cannot be a date that is farther in the past than a previous COD filing (standalone or AR). - The effective date can be the same effective date as another COD filing (standalone or AR). If this is the case: - COD filing that was filed most recently is the most current director information. """ msg = [] # get effective datetime string from filing try: effective_datetime_str = cod['filing']['header']['effectiveDate'] except KeyError: return {'error': babel('No effective date provided.')} # convert string to datetime try: effective_datetime_utc = datetime.fromisoformat(effective_datetime_str) except ValueError: return {'error': babel('Invalid ISO format for effective date.')} # check if effective datetime is in the future if effective_datetime_utc > datetime.utcnow(): msg.append({'error': babel('Filing cannot have a future effective date.')}) # convert to legislation timezone and then get date only effective_date_leg = LegislationDatetime.as_legislation_timezone(effective_datetime_utc).date() # check if effective date is before their incorporation date founding_date_leg = LegislationDatetime.as_legislation_timezone(business.founding_date).date() if effective_date_leg < founding_date_leg: msg.append({'error': babel('Effective date cannot be before businesses founding date.')}) # check if effective date is before their most recent COD or AR date last_cod_filing = Filing.get_most_recent_legal_filing(business.id, Filing.FILINGS['changeOfDirectors']['name']) if last_cod_filing: last_cod_date_leg = LegislationDatetime.as_legislation_timezone(last_cod_filing.effective_date).date() if effective_date_leg < last_cod_date_leg: msg.append({'error': babel('Effective date cannot be before another Change of Director filing.')}) return msg
def update_business_profile(business: Business, profile_info: Dict) -> Dict: """Set the legal type of the business.""" if not business or not profile_info: return {'error': babel('Business and profile_info required.')} # contact phone is optional phone = profile_info.get('phone', '') error = {'error': 'Unknown handling'} if email := profile_info.get('email'): # assume the JSONSchema ensures it is a valid email format token = AccountService.get_bearer_token() account_svc_entity_url = current_app.config['ACCOUNT_SVC_ENTITY_URL'] # Create an entity record data = json.dumps({ 'email': email, 'phone': phone, 'phoneExtension': '' }) url = ''.join( [account_svc_entity_url, '/', business.identifier, '/contacts']) rv = requests.post( url=url, headers={ **AccountService.CONTENT_TYPE_JSON, 'Authorization': AccountService.BEARER + token }, data=data, timeout=AccountService.timeout) if rv.status_code in (HTTPStatus.OK, HTTPStatus.CREATED): error = None if rv.status_code == HTTPStatus.NOT_FOUND: error = {'error': 'No business profile found.'} if rv.status_code == HTTPStatus.METHOD_NOT_ALLOWED: error = { 'error': 'Service account missing privileges to update business profiles' } if rv.status_code == HTTPStatus.BAD_REQUEST and \ 'DATA_ALREADY_EXISTS' in rv.text: put = requests.put( url=''.join([account_svc_entity_url, '/', business.identifier]), headers={ **AccountService.CONTENT_TYPE_JSON, 'Authorization': AccountService.BEARER + token }, data=data, timeout=AccountService.timeout) if put.status_code in (HTTPStatus.OK, HTTPStatus.CREATED): error = None else: error = { 'error': 'Unable to update existing business profile.' }
def validate_correction_effective_date(filing: Dict, corrected_filing: Dict) -> Optional[Dict]: """Validate that effective dates follow the rules. Currently effective dates cannot be changed. """ if new_effective_date := filing.get('filing', {}).get('header', {}).get('effectiveDate'): if new_effective_date != corrected_filing.get('filing', {}).get('header', {}).get('effectiveDate'): return {'error': babel('The effective date of a filing cannot be changed in a correction.')}
def register_bootstrap(bootstrap: RegistrationBootstrap, business_name: str) \ -> Union[HTTPStatus, Dict]: """Return either a new bootstrap registration or an error struct.""" if not bootstrap: return {'error': babel('An account number must be provided.')} rv = AccountService.create_affiliation(account=bootstrap.account, business_registration=bootstrap.identifier, business_name=business_name) if rv == HTTPStatus.OK: return HTTPStatus.OK with contextlib.suppress(Exception): AccountService.delete_affiliation(account=bootstrap.account, business_registration=bootstrap.identifier) return {'error': babel('Unable to create bootstrap registration.')}
def validate_cooperative_documents(incorporation_json): """Return an error or warning message based on the cooperative documents validation rules. Rules: - The documents are provided. - Document IDs are unique. """ # Setup msg = [] rules_file_key = incorporation_json['filing']['incorporationApplication'][ 'cooperative']['rulesFileKey'] rules_file_name = incorporation_json['filing']['incorporationApplication'][ 'cooperative']['rulesFileName'] memorandum_file_key = incorporation_json['filing'][ 'incorporationApplication']['cooperative']['memorandumFileKey'] memorandum_file_name = incorporation_json['filing'][ 'incorporationApplication']['cooperative']['memorandumFileName'] # Validate key values exist if not rules_file_key: msg.append({'error': babel('A valid rules key is required.')}) if not rules_file_name: msg.append({'error': babel('A valid rules file name is required.')}) if not memorandum_file_key: msg.append({'error': babel('A valid memorandum key is required.')}) if not memorandum_file_name: msg.append( {'error': babel('A valid memorandum file name is required.')}) if msg: return msg rules_err = validate_pdf(rules_file_key) if rules_err: return rules_err memorandum_err = validate_pdf(memorandum_file_key) if memorandum_err: return memorandum_err return None
def validate_correction_ia(filing: Dict) -> Optional[Error]: """Validate correction of Incorporation Application.""" if not (corrected_filing # pylint: disable=superfluous-parens; needed to pass pylance := Filing.find_by_id( filing['filing']['correction']['correctedFilingId'])): return Error( HTTPStatus.BAD_REQUEST, [{ 'error': babel('Missing the id of the filing being corrected.') }])
def type_change_validation(filing): """Validate type change.""" msg = [] legal_type_path: Final = '/filing/alteration/business/legalType' # you must alter to a bc benefit company if get_str(filing, legal_type_path) != Business.LegalTypes.BCOMP.value: msg.append({'error': babel('Your business type has not been updated to a BC Benefit Company.'), 'path': legal_type_path}) return msg return []
def create_bootstrap(account: int) -> Union[Dict, RegistrationBootstrap]: """Return either a new bootstrap registration or an error struct.""" if not account: return {'error': babel('An account number must be provided.')} bootstrap = RegistrationBootstrap(account=account) allowed_encoded = string.ascii_letters + string.digits # try to create a bootstrap registration with a unique ID for _ in range(5): bootstrap.identifier = 'T' + ''.join(secrets.choice(allowed_encoded) for _ in range(9)) try: bootstrap.save() return bootstrap except FlushError: pass # we try again except Exception: break return {'error': babel('Unable to create bootstrap registration.')}
def update_aliases(business: Business, aliases) -> Dict: """Update the aliases of the business.""" if not business: return { 'error': babel('Business required before alternate names can be set.') } for alias in aliases: if (alias_id := alias.get('id')) and \ (existing_alias := next((x for x in business.aliases.all() if str(x.id) == alias_id), None)):
def update_filing_court_order(filing_submission: Filing, court_order_json: Dict) -> Optional[Dict]: """Update the court_order info for a Filing.""" if not Filing: return {'error': babel('Filing required before alternate names can be set.')} filing_submission.court_order_file_number = court_order_json.get('fileNumber') filing_submission.court_order_effect_of_order = court_order_json.get('effectOfOrder') with suppress(IndexError, KeyError, TypeError, ValueError): filing_submission.court_order_date = datetime.fromisoformat(court_order_json.get('orderDate')) return None
def company_name_validation(filing): """Validate share structure.""" msg = [] nr_path: Final = '/filing/alteration/nameRequest/nrNumber' if nr_number := get_str(filing, nr_path): # ensure NR is approved or conditionally approved nr_response = namex.query_nr_number(nr_number) validation_result = namex.validate_nr(nr_response) if not nr_response['requestTypeCd'] in ('CCR', 'CCP', 'BEC'): msg.append({ 'error': babel( 'Alteration only available for Change of Name Name requests.' ), 'path': nr_path }) if not validation_result['is_approved']: msg.append({ 'error': babel('Alteration of Name Request is not approved.'), 'path': nr_path }) # ensure NR request has the same legal name legal_name_path: Final = '/filing/alteration/nameRequest/legalName' legal_name = get_str(filing, legal_name_path) nr_name = namex.get_approved_name(nr_response) if nr_name != legal_name: msg.append({ 'error': babel( 'Alteration of Name Request has a different legal name.'), 'path': legal_name_path })
def post(): """Create an Incorporation Filing, else error out.""" json_input = request.get_json() try: filing_account_id = json_input['filing']['header']['accountId'] filing_type = json_input['filing']['header']['name'] if filing_type != Filing.FILINGS['incorporationApplication'][ 'name']: raise TypeError except (TypeError, KeyError): return { 'error': babel('Requires a minimal Incorporation Filing.') }, HTTPStatus.BAD_REQUEST # @TODO rollback bootstrap if there is A failure, awaiting changes in the affiliation service bootstrap = RegistrationBootstrapService.create_bootstrap( filing_account_id) if not isinstance(bootstrap, RegistrationBootstrap): return { 'error': babel('Unable to create Incorporation Filing.') }, HTTPStatus.SERVICE_UNAVAILABLE try: business_name = json_input['filing']['incorporationApplication'][ 'nameRequest']['nrNumber'] except KeyError: business_name = bootstrap.identifier rv = RegistrationBootstrapService.register_bootstrap( bootstrap, business_name) if not isinstance(rv, HTTPStatus): with suppress(Exception): bootstrap.delete() return { 'error': babel('Unable to create Incorporation Filing.') }, HTTPStatus.SERVICE_UNAVAILABLE return ListFilingResource.put(bootstrap.identifier, None)
def validate(business: Business, filing_json: Dict) -> Error: # pylint: disable=too-many-branches """Validate the filing JSON.""" err = validate_against_schema(filing_json) if err: return err err = None # check if this is a correction - if yes, ignore all other filing types in the filing since they will be validated # differently in a future version of corrections if 'correction' in filing_json['filing'].keys(): err = correction_validate(business, filing_json) if err: return err # For now the correction validators will get called here, these might be the same rules # so these 2 sections could get collapsed for k in filing_json['filing'].keys(): # Check if the JSON key exists in the FILINGS reference Dictionary if Filing.FILINGS.get(k, None): if k == Filing.FILINGS['changeOfAddress'].get('name'): err = coa_validate(business, filing_json) elif k == Filing.FILINGS['incorporationApplication'].get( 'name'): err = validate_correction_ia(filing_json) if err: return err elif 'dissolution' in filing_json['filing'].keys() \ and (dissolution_type := filing_json['filing']['dissolution'].get('dissolutionType', None)) \ and dissolution_type == 'voluntary': err = dissolution_validate(business, filing_json) if err: return err legal_type = get_str(filing_json, '/filing/business/legalType') if legal_type == Business.LegalTypes.COOP.value: if 'specialResolution' in filing_json['filing'].keys(): err = special_resolution_validate(business, filing_json) else: err = Error( HTTPStatus.BAD_REQUEST, [{ 'error': babel('Special Resolution is required.'), 'path': '/filing/specialResolution' }]) if err: return err
def validate(business: Business, cod: Dict) -> Error: """Validate the Change of Directors filing.""" if not business or not cod: return Error(HTTPStatus.BAD_REQUEST, [{'error': babel('A valid business and filing are required.')}]) msg = [] msg_directors_addresses = validate_directors_addresses(cod) if msg_directors_addresses: msg += msg_directors_addresses msg_effective_date = validate_effective_date(business, cod) if msg_effective_date: msg += msg_effective_date if msg: return Error(HTTPStatus.BAD_REQUEST, msg) return None
def validate(business: Business, filing: Dict) -> Error: # pylint: disable=too-many-branches """Validate the Alteration filing.""" if not business or not filing: return Error( HTTPStatus.BAD_REQUEST, [{ 'error': babel('A valid business and filing are required.') }]) msg = [] msg.extend(company_name_validation(filing)) msg.extend(share_structure_validation(filing)) msg.extend(court_order_validation(filing)) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) return None
def update_aliases(business: Business, aliases) -> Dict: """Update the aliases of the business.""" if not business: return { 'error': babel('Business required before alternate names can be set.') } for alias in aliases: if alias_id := alias.get('id'): existing_alias = next( (x for x in business.aliases.all() if str(x.id) == alias_id), None) existing_alias.alias = alias['name'].upper() else: new_alias = Alias(alias=alias['name'].upper(), type=Alias.AliasType.TRANSLATION.value) business.aliases.append(new_alias)