From c24d261a0c7151ad6b323c5aa82321feac270719 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Thu, 25 Jan 2024 10:50:57 -0800 Subject: [PATCH] Initial changes for adding in displayName for legalName changes (SP/GP). Minor fix Small tweak, code complexity Fix unit test re-do feature flag Fix LD flags Remove updating business name in entities table, we no longer use this field. It comes from LEAR. Rewire loadBusiness to query lear, compute the business name off of LEAR's results 15603 - Initial changes for adding in displayName for legalName changes (SP/GP). (#2690) * Initial changes for adding in displayName for legalName changes (SP/GP). * Minor fix * Small tweak, code complexity * Fix unit test * re-do feature flag * Fix LD flags --- auth-api/src/auth_api/__init__.py | 2 +- auth-api/src/auth_api/config.py | 6 +++ auth-api/src/auth_api/models/affiliation.py | 11 ---- auth-api/src/auth_api/services/__init__.py | 2 - auth-api/src/auth_api/services/affiliation.py | 38 +++++++++++--- auth-api/src/auth_api/services/flags.py | 33 +++++++----- auth-api/tests/unit/services/test_flags.py | 6 +-- auth-web/package-lock.json | 4 +- auth-web/package.json | 2 +- .../SearchBusinessNameRequest.vue | 1 - .../ManageBusinessDialog.vue | 6 --- auth-web/src/models/affiliation.ts | 9 ++++ auth-web/src/models/business.ts | 13 ++--- auth-web/src/services/business.services.ts | 6 +-- auth-web/src/stores/business.ts | 52 +++++++++---------- auth-web/src/util/constants.ts | 1 + .../views/auth/staff/StaffDashboardView.vue | 5 +- 17 files changed, 107 insertions(+), 90 deletions(-) diff --git a/auth-api/src/auth_api/__init__.py b/auth-api/src/auth_api/__init__.py index 93dece9133..6ce19ec257 100644 --- a/auth-api/src/auth_api/__init__.py +++ b/auth-api/src/auth_api/__init__.py @@ -32,10 +32,10 @@ from auth_api.extensions import mail from auth_api.models import db, ma from auth_api.resources import endpoints +from auth_api.services.flags import flags from auth_api.utils.cache import cache from auth_api.utils.run_version import get_run_version from auth_api.utils.util_logging import setup_logging -from auth_api.services import flags setup_logging(os.path.join(_Config.PROJECT_ROOT, 'logging.conf')) # important to do this first diff --git a/auth-api/src/auth_api/config.py b/auth-api/src/auth_api/config.py index 865a9a10d4..e7853227ed 100644 --- a/auth-api/src/auth_api/config.py +++ b/auth-api/src/auth_api/config.py @@ -133,6 +133,12 @@ class _Config: # pylint: disable=too-few-public-methods LEGAL_API_VERSION_2 = os.getenv('LEGAL_API_VERSION_2', '') LEAR_AFFILIATION_DETAILS_URL = f'{LEGAL_API_URL + LEGAL_API_VERSION_2}/businesses/search' + + # Temporary until legal names is implemented. + LEGAL_API_ALTERNATE_URL = os.getenv('LEGAL_API_ALTERNATE_URL', '') + # Temporary until legal names is implemented. + LEAR_ALTERNATE_AFFILIATION_DETAILS_URL = f'{LEGAL_API_ALTERNATE_URL + LEGAL_API_VERSION_2}/businesses/search' + NAMEX_AFFILIATION_DETAILS_URL = f'{NAMEX_API_URL}/requests/search' # NATS Config diff --git a/auth-api/src/auth_api/models/affiliation.py b/auth-api/src/auth_api/models/affiliation.py index f24c6683fe..37f2f53add 100644 --- a/auth-api/src/auth_api/models/affiliation.py +++ b/auth-api/src/auth_api/models/affiliation.py @@ -18,12 +18,9 @@ from __future__ import annotations from typing import List -from flask import current_app from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import contains_eager, relationship -from auth_api.utils.enums import CorpType - from .base_model import VersionedModel from .db import db from .entity import Entity as EntityModel @@ -53,14 +50,6 @@ def filter_environment(cls, environment: str): query = query.filter(Affiliation.environment.is_(None)) return query - @property - def affiliation_details_url(self) -> str: - """The url of the source service containing the affiliations full data.""" - if self.entity.corp_type_code == CorpType.NR.value: - return current_app.config.get('NAMEX_AFFILIATION_DETAILS_URL') - # only have LEAR and NAMEX affiliations - return current_app.config.get('LEAR_AFFILIATION_DETAILS_URL') - @classmethod def find_affiliation_by_org_and_entity_ids(cls, org_id, entity_id, environment) -> Affiliation: """Return an affiliation for the provided org and entity ids.""" diff --git a/auth-api/src/auth_api/services/__init__.py b/auth-api/src/auth_api/services/__init__.py index 161964ee91..fd5f757ff9 100644 --- a/auth-api/src/auth_api/services/__init__.py +++ b/auth-api/src/auth_api/services/__init__.py @@ -33,5 +33,3 @@ from .user import User from .user_settings import UserSettings from .flags import Flags - -flags = Flags() diff --git a/auth-api/src/auth_api/services/affiliation.py b/auth-api/src/auth_api/services/affiliation.py index b59be84118..ce41c3e976 100644 --- a/auth-api/src/auth_api/services/affiliation.py +++ b/auth-api/src/auth_api/services/affiliation.py @@ -34,6 +34,7 @@ from auth_api.models.membership import Membership as MembershipModel from auth_api.schemas import AffiliationSchema from auth_api.services.entity import Entity as EntityService +from auth_api.services.flags import flags from auth_api.services.org import Org as OrgService from auth_api.services.user import User as UserService from auth_api.utils.enums import ActivityAction, CorpType, NRActionCodes, NRNameStatus, NRStatus @@ -392,12 +393,23 @@ def fix_stale_affiliations(org_id: int, entity_details: Dict, environment: str = current_app.logger.debug('>fix_stale_affiliations') + @staticmethod + def _affiliation_details_url(affiliation: AffiliationModel) -> str: + """Determine url to call for affiliation details.""" + # only have LEAR and NAMEX affiliations + if affiliation.entity.corp_type_code == CorpType.NR.value: + return current_app.config.get('NAMEX_AFFILIATION_DETAILS_URL') + # Temporary until legal names is implemented. + if flags.is_on('enable-alternate-names-mbr', default=False): + return current_app.config.get('LEAR_ALTERNATE_AFFILIATION_DETAILS_URL') + return current_app.config.get('LEAR_AFFILIATION_DETAILS_URL') + @staticmethod async def get_affiliation_details(affiliations: List[AffiliationModel]) -> List: """Return affiliation details by calling the source api.""" url_identifiers = {} # i.e. turns into { url: [identifiers...] } for affiliation in affiliations: - url = affiliation.affiliation_details_url + url = Affiliation._affiliation_details_url(affiliation) url_identifiers.setdefault(url, [affiliation.entity.business_identifier])\ .append(affiliation.entity.business_identifier) @@ -428,8 +440,7 @@ def sort_key(item): raise ServiceUnavailableException('Failed to get affiliation details') from err @staticmethod - def _combine_affiliation_details(details): - """Parse affiliation details responses and combine draft entities with NRs if applicable.""" + def _group_details(details): name_requests = {} businesses = [] drafts = [] @@ -450,17 +461,24 @@ def _combine_affiliation_details(details): drafts = [ {'draftType': CorpType.RTMP.value if draft['legalType'] in draft_reg_types else CorpType.TMP.value, **draft} for draft in data[drafts_key]] + return name_requests, businesses, drafts + + @staticmethod + def _update_draft_type_for_amalgamation_nr(business): + if business.get('draftType', None) \ + and business['nameRequest']['request_action_cd'] == NRActionCodes.AMALGAMATE.value: + business['draftType'] = CorpType.ATMP.value + return business + @staticmethod + def _combine_nrs(name_requests, businesses, drafts): # combine NRs for business in drafts + businesses: # Only drafts have nrNumber coming back from legal-api. if 'nrNumber' in business and (nr_num := business['nrNumber']): if business['nrNumber'] in name_requests: business['nameRequest'] = name_requests[nr_num]['nameRequest'] - # Update the draft type if the draft NR request is for amalgamation - if business.get('draftType', None) \ - and business['nameRequest']['request_action_cd'] == NRActionCodes.AMALGAMATE.value: - business['draftType'] = CorpType.ATMP.value + business = Affiliation._update_draft_type_for_amalgamation_nr(business) # Remove the business if the draft associated to the NR is consumed. if business['nameRequest']['stateCd'] == NRStatus.CONSUMED.value: drafts.remove(business) @@ -471,6 +489,12 @@ def _combine_affiliation_details(details): return [name_request for nr_num, name_request in name_requests.items()] + drafts + businesses + @staticmethod + def _combine_affiliation_details(details): + """Parse affiliation details responses and combine draft entities with NRs if applicable.""" + name_requests, businesses, drafts = Affiliation._group_details(details) + return Affiliation._combine_nrs(name_requests, businesses, drafts) + @staticmethod def _get_nr_details(nr_number: str): """Return NR details by calling legal-api.""" diff --git a/auth-api/src/auth_api/services/flags.py b/auth-api/src/auth_api/services/flags.py index 5d039e3381..ac72235a99 100644 --- a/auth-api/src/auth_api/services/flags.py +++ b/auth-api/src/auth_api/services/flags.py @@ -1,4 +1,4 @@ -# Copyright © 2019 Province of British Columbia +# Copyright © 2022 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. @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Manage the Feature Flags initialization, setup and service.""" +import logging from flask import current_app from ldclient import get as ldclient_get, set_config as ldclient_set_config # noqa: I001 from ldclient.config import Config # noqa: I005 @@ -48,24 +49,18 @@ def init_app(self, app): if self.sdk_key or app.env != 'production': - if app.env == 'production': - config = Config(sdk_key=self.sdk_key) - else: + if app.env == 'testing': factory = Files.new_data_source(paths=['flags.json'], auto_update=True) config = Config(sdk_key=self.sdk_key, update_processor_class=factory, send_events=False) + else: + config = Config(sdk_key=self.sdk_key) ldclient_set_config(config) client = ldclient_get() app.extensions['featureflags'] = client - app.teardown_appcontext(self.teardown) - - def teardown(self, exception): # pylint: disable=unused-argument; flask method signature - """Destroy all objects created by this extension.""" - client = current_app.extensions['featureflags'] - client.close() def _get_client(self): try: @@ -75,6 +70,7 @@ def _get_client(self): self.init_app(current_app) client = current_app.extensions['featureflags'] except KeyError: + logging.warning("Couldn\'t retrieve launch darkly client from extensions.") client = None return client @@ -89,24 +85,33 @@ def _user_as_key(user: User): .set('firstName', user.firstname)\ .set('lastName', user.lastname).build() - def is_on(self, flag: str, user: User = None) -> bool: + def is_on(self, flag: str, default: bool = False, user: User = None) -> bool: """Assert that the flag is set for this user.""" client = self._get_client() + if not client: + return default + if user: flag_user = self._user_as_key(user) else: flag_user = self._get_anonymous_user() - return bool(client.variation(flag, flag_user, None)) + return bool(client.variation(flag, flag_user, default)) - def value(self, flag: str, user: User = None) -> bool: + def value(self, flag: str, default=None, user: User = None): """Retrieve the value of the (flag, user) tuple.""" client = self._get_client() + if not client: + return default + if user: flag_user = self._user_as_key(user) else: flag_user = self._get_anonymous_user() - return client.variation(flag, flag_user, None) + return client.variation(flag, flag_user, default) + + +flags = Flags() diff --git a/auth-api/tests/unit/services/test_flags.py b/auth-api/tests/unit/services/test_flags.py index 3495e198b7..e64d92dcc2 100644 --- a/auth-api/tests/unit/services/test_flags.py +++ b/auth-api/tests/unit/services/test_flags.py @@ -27,7 +27,7 @@ def setup(): """Initialize app with dev env for testing.""" global app app = Flask(__name__) - app.env = 'development' + app.env = 'testing' def test_flags_constructor_no_app(setup): @@ -152,8 +152,8 @@ def test_flags_read_flag_values_unique_user(setup, test_name, flag_name, expecte with app.app_context(): flags = Flags() flags.init_app(app) - val = flags.value(flag_name, user) - flag_on = flags.is_on(flag_name, user) + val = flags.value(flag_name, user=user) + flag_on = flags.is_on(flag_name, user=user, default=False) assert val == expected assert flag_on diff --git a/auth-web/package-lock.json b/auth-web/package-lock.json index a5ed969b6a..082de69c38 100644 --- a/auth-web/package-lock.json +++ b/auth-web/package-lock.json @@ -1,12 +1,12 @@ { "name": "auth-web", - "version": "2.4.59", + "version": "2.4.61", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "auth-web", - "version": "2.4.59", + "version": "2.4.61", "dependencies": { "@bcrs-shared-components/base-address": "2.0.3", "@bcrs-shared-components/bread-crumb": "1.0.8", diff --git a/auth-web/package.json b/auth-web/package.json index 687a3a8541..fa7d1e917d 100644 --- a/auth-web/package.json +++ b/auth-web/package.json @@ -1,6 +1,6 @@ { "name": "auth-web", - "version": "2.4.59", + "version": "2.4.61", "appName": "Auth Web", "sbcName": "SBC Common Components", "private": true, diff --git a/auth-web/src/components/auth/manage-business/SearchBusinessNameRequest.vue b/auth-web/src/components/auth/manage-business/SearchBusinessNameRequest.vue index 7b26028ceb..ce9517141a 100644 --- a/auth-web/src/components/auth/manage-business/SearchBusinessNameRequest.vue +++ b/auth-web/src/components/auth/manage-business/SearchBusinessNameRequest.vue @@ -116,7 +116,6 @@ import { mapActions } from 'pinia' }, methods: { ...mapActions(useBusinessStore, [ - 'updateBusinessName', 'updateFolioNumber' ]) } diff --git a/auth-web/src/components/auth/manage-business/manage-business-dialog/ManageBusinessDialog.vue b/auth-web/src/components/auth/manage-business/manage-business-dialog/ManageBusinessDialog.vue index abc07cc883..f66da801ef 100644 --- a/auth-web/src/components/auth/manage-business/manage-business-dialog/ManageBusinessDialog.vue +++ b/auth-web/src/components/auth/manage-business/manage-business-dialog/ManageBusinessDialog.vue @@ -708,12 +708,6 @@ export default defineComponent({ if (addResponse?.status !== StatusCodes.CREATED) { emit('unknown-error') } - // try to update business name - const businessResponse = await businessStore.updateBusinessName(businessIdentifier.value) - // check if update didn't succeed - if (businessResponse?.status !== StatusCodes.OK) { - emit('unknown-error') - } // let parent know that add was successful emit('add-success', businessIdentifier.value) } catch (exception) { diff --git a/auth-web/src/models/affiliation.ts b/auth-web/src/models/affiliation.ts index ad40cd4fee..7e859b6921 100644 --- a/auth-web/src/models/affiliation.ts +++ b/auth-web/src/models/affiliation.ts @@ -55,6 +55,14 @@ export interface AffiliationFilter { actions?: string } +export interface AlternateNames { + entityType?: string + identifier?: string + nameRegisteredDate?: string + nameStartDate?: string + operatingName?: string +} + export interface AffiliationResponse { identifier?: string draftType?: CorpTypes @@ -62,6 +70,7 @@ export interface AffiliationResponse { businessNumber?: string name?: string legalName?: string + alternateNames?: AlternateNames[] contacts?: Contact[] corpType?: CorpType corpSubType?: CorpType diff --git a/auth-web/src/models/business.ts b/auth-web/src/models/business.ts index a2ce9951bc..fb2aa83e1d 100644 --- a/auth-web/src/models/business.ts +++ b/auth-web/src/models/business.ts @@ -1,6 +1,6 @@ +import { AffiliationInviteInfo, AlternateNames } from '@/models/affiliation' import { AmalgamationTypes, FilingTypes, NrRequestActionCodes, NrRequestTypeCodes } from '@bcrs-shared-components/enums' import { CorpTypes, LearFilingTypes, NrTargetTypes } from '@/util/constants' -import { AffiliationInviteInfo } from '@/models/affiliation' import { Contact } from './contact' export interface LoginPayload { @@ -57,11 +57,6 @@ export interface Businesses { entities: Business[] } -export interface UpdateBusinessNamePayload { - businessIdentifier: string - name: string -} - // see https://github.com/bcgov/business-schemas/blob/master/src/registry_schemas/schemas/name_request.json export interface NameRequest { actions?: Array @@ -145,7 +140,9 @@ export interface PasscodeResetLoad { } export interface LearBusiness { - identifier: string, - legalName: string, + identifier: string + legalName: string + legalType: string + alternateNames: AlternateNames[] taxId?: string } diff --git a/auth-web/src/services/business.services.ts b/auth-web/src/services/business.services.ts index 1a57ec7bb1..89cfc5e6d3 100644 --- a/auth-web/src/services/business.services.ts +++ b/auth-web/src/services/business.services.ts @@ -1,5 +1,5 @@ import { BNRequest, ResubmitBNRequest } from '@/models/request-tracker' -import { Business, BusinessRequest, FolioNumberload, PasscodeResetLoad, UpdateBusinessNamePayload } from '@/models/business' +import { Business, BusinessRequest, FolioNumberload, PasscodeResetLoad } from '@/models/business' import { AxiosResponse } from 'axios' import CommonUtils from '@/util/common-util' import ConfigHelper from '@/util/config-helper' @@ -51,10 +51,6 @@ export default class BusinessService { return axios.patch(`${ConfigHelper.getAuthAPIUrl()}/entities/${folioNumber.businessIdentifier}`, folioNumber) } - static async updateBusinessName (updatePayload: UpdateBusinessNamePayload): Promise> { - return axios.patch(`${ConfigHelper.getAuthAPIUrl()}/entities/${updatePayload.businessIdentifier}`, updatePayload) - } - static async resetBusinessPasscode (passcodeResetLoad: PasscodeResetLoad): Promise> { return axios.patch(`${ConfigHelper.getAuthAPIUrl()}/entities/${passcodeResetLoad.businessIdentifier}`, { diff --git a/auth-web/src/stores/business.ts b/auth-web/src/stores/business.ts index a8f174517b..c2ac3fb6ea 100644 --- a/auth-web/src/stores/business.ts +++ b/auth-web/src/stores/business.ts @@ -12,6 +12,7 @@ import { } from '@/util/constants' import { AffiliationResponse, + AlternateNames, CreateRequestBody as CreateAffiliationRequestBody, CreateNRAffiliationRequestBody, NameRequestResponse @@ -48,12 +49,30 @@ export const useBusinessStore = defineStore('business', () => { return useOrgStore().currentOrganization }) + function determineDisplayName ( + legalName: string, + legalType: string, + identifier: string, + alternateNames: AlternateNames[] + ): string { + if (!LaunchDarklyService.getFlag(LDFlags.AlternateNamesMbr, false)) { + return legalName + } + if ([CorpTypes.SOLE_PROP, CorpTypes.PARTNERSHIP].includes(legalType as CorpTypes)) { + // Intentionally show blank, if the alternate name is not found. This is to avoid showing the legal name. + return alternateNames?.find(alt => alt.identifier === identifier)?.operatingName + } else { + return legalName + } + } + /* Internal function to build the business object. */ function buildBusinessObject (resp: AffiliationResponse): Business { return { businessIdentifier: resp.identifier, ...(resp.businessNumber && { businessNumber: resp.businessNumber }), - ...(resp.legalName && { name: resp.legalName }), + ...(resp.legalName && + { name: determineDisplayName(resp.legalName, resp.legalType, resp.identifier, resp.alternateNames) }), ...(resp.contacts && { contacts: resp.contacts }), ...((resp.draftType || resp.legalType) && { corpType: { code: resp.draftType || resp.legalType } }), ...(resp.legalType && { corpSubType: { code: resp.legalType } }), @@ -210,10 +229,15 @@ export const useBusinessStore = defineStore('business', () => { async function loadBusiness () { const businessIdentifier = ConfigHelper.getFromSession(SessionStorageKeys.BusinessIdentifierKey) + // Need to look at LEAR, because it has the up-to-date names. + const learBusiness = await searchBusiness(businessIdentifier) const response = await BusinessService.getBusiness(businessIdentifier) if (response?.data && response.status === 200) { ConfigHelper.addToSession(SessionStorageKeys.BusinessIdentifierKey, response.data.businessIdentifier) - state.currentBusiness = response.data + const business = response.data + business.name = determineDisplayName( + learBusiness.legalName, learBusiness.legalType, learBusiness.identifier, learBusiness.alternateNames) + state.currentBusiness = business return response.data } } @@ -229,29 +253,6 @@ export const useBusinessStore = defineStore('business', () => { return OrgService.createAffiliation(currentOrganization.value.id, requestBody) } - async function updateBusinessName (businessNumber: string) { - try { - const businessResponse = await BusinessService.searchBusiness(businessNumber) - if ((businessResponse?.status === 200) && businessResponse?.data?.business?.legalName) { - const updateBusinessResponse = await BusinessService.updateBusinessName({ - businessIdentifier: businessNumber, - name: businessResponse.data.business.legalName - }) - if (updateBusinessResponse?.status === 200) { - return updateBusinessResponse - } - } - throw Error('update failed') - } catch (error) { - // delete the created affiliation if the update failed for avoiding orphan records - // unable to do these from backend, since it causes a circular dependency - await OrgService.removeAffiliation(currentOrganization.value.id, businessNumber, undefined, false) - return { - errorMsg: 'Cannot add business due to some technical reasons' - } - } - } - async function addNameRequest (requestBody: CreateNRAffiliationRequestBody) { // Create an affiliation between implicit org and requested business return OrgService.createNRAffiliation(currentOrganization.value.id, requestBody) @@ -517,7 +518,6 @@ export const useBusinessStore = defineStore('business', () => { syncBusinesses, loadBusiness, addBusiness, - updateBusinessName, addNameRequest, createNamedBusiness, searchBusiness, diff --git a/auth-web/src/util/constants.ts b/auth-web/src/util/constants.ts index 4e4f3a8f20..6ab4248210 100644 --- a/auth-web/src/util/constants.ts +++ b/auth-web/src/util/constants.ts @@ -407,6 +407,7 @@ export enum Permission { } export enum LDFlags { + AlternateNamesMbr = 'enable-alternate-names-mbr', AffiliationInvitationRequestAccess = 'enable-affiliation-invitation-request-access', BannerText = 'banner-text', BusSearchLink = 'bus-search-staff-link', diff --git a/auth-web/src/views/auth/staff/StaffDashboardView.vue b/auth-web/src/views/auth/staff/StaffDashboardView.vue index 04f2ed183d..3b4ddf9e55 100644 --- a/auth-web/src/views/auth/staff/StaffDashboardView.vue +++ b/auth-web/src/views/auth/staff/StaffDashboardView.vue @@ -195,6 +195,7 @@