"""Logic module for new applications and roles.""" import uuid from typing import Callable from flask import g from owsresponse import response from pythonfeatures import pythonfeatures from pythonfeatures.constants import split as split_constants from sqlalchemy.orm import Session from account.connectors.mysql import db_read_only_session_wrap from account.constants import constants, features from account.constants.features import FEATURES from account.models import ( application as app_model, feature as feature_model, types, vendor as vendor_model, ) from account.models.types import TenantApplication from account.utils.dataloader_util import format_for_dataloader VendorApplicationChecker = Callable[[types.VendorAttributes], types.TenantApplication | None] ENABLED_APPLICATIONS = [ app_model.BANKING_TAX_APP, app_model.COLLABORATORS_APP, app_model.CUSTOMER_ACCOUNTING_APP, app_model.FANSIFTER_APP, app_model.INSIGHTS_APP, app_model.SONGWHIP_APP, app_model.WORKSTATION_ANALYTICS_APP, app_model.WORKSTATION_CATALOG_APP, app_model.WORKSTATION_MARKETING_APP, app_model.SETTINGS_APP, ] def get_insights_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get insights application for a vendor based on the company brand. Args: vendor_attributes (TenantApplication): Vendor attributes. Return: TenantApplication | None: Insights application with a base role. """ if vendor_attributes['company_brand']['name'] == 'knr': return None return TenantApplication( application_id=app_model.INSIGHTS_APP, roles=[app_model.INSIGHTS_BASE_ROLE], url=app_model.get_application_url( app_model.INSIGHTS_APP, vendor_attributes['company_brand']['name'] ), ) def get_ws_analytics_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get workstation analytics application for a vendor based on the company brand and feature controls. Args: vendor_attributes (VendorAttributes): Vendor attributes. Return: TenantApplication | None: Workstation analytics application with a base role. """ if vendor_attributes['company_brand']['name'] not in ['theorchard', 'altafonte']: return None required_features = [ FEATURES.WORKSTATION_PHYSICAL_REPORTING, FEATURES.WORKSTATION_ANALYTICS_OVERVIEW, ] if not any(feature in vendor_attributes['features'] for feature in required_features): return None return TenantApplication( application_id=app_model.WORKSTATION_ANALYTICS_APP, roles=[app_model.WORKSTATION_ANALYTICS_BASE_ROLE], url=app_model.get_application_url( app_model.WORKSTATION_ANALYTICS_APP, vendor_attributes['company_brand']['name'] ), ) def get_ws_marketing_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get workstation marketing application for a vendor based on the company brand and feature controls. Args: vendor_attributes (VendorAttributes): Vendor attributes. Return: TenantApplication | None: Workstation marketing application with a base role. """ if vendor_attributes['company_brand']['name'] in ['knr', 'awal']: return None has_feature = FEATURES.MARKETPLACE in vendor_attributes['features'] if not has_feature: return None has_roles = [app_model.WORKSTATION_MARKETING_ROLE] if FEATURES.ORCHARD_ADVERTISING in vendor_attributes['features']: has_roles = [app_model.WORKSTATION_MARKETING_ROLE, app_model.WORKSTATION_ADVERTISING_ROLE] return TenantApplication( application_id=app_model.WORKSTATION_MARKETING_APP, roles=has_roles, url=app_model.get_application_url( app_model.WORKSTATION_MARKETING_APP, vendor_attributes['company_brand']['name'] ), ) def get_customer_accounting_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get customer accounting application for a vendor based on the company brand and feature controls. Args: vendor_attributes (VendorAttributes): Vendor attributes. Return: TenantApplication]: Customer accounting application with a base role. """ if not vendor_attributes['migrated_to_abacus']: return None return TenantApplication( application_id=app_model.CUSTOMER_ACCOUNTING_APP, roles=[app_model.CUSTOMER_ACCOUNTING_BASE_ROLE], url=app_model.get_application_url( app_model.CUSTOMER_ACCOUNTING_APP, vendor_attributes['company_brand']['name'] ), ) def get_ws_catalog_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get workstation catalog application for a vendor based on the company brand and feature controls. Args: vendor_attributes (VendorAttributes): Vendor attributes. Return: TenantApplication | None: Workstation analytics application with a base role. """ if vendor_attributes['company_brand']['name'] == 'knr': return None required_features = [ FEATURES.AUDIO_RELEASE_BUILDER, FEATURES.MUSIC_VIDEO_RELEASE_BUILDER, FEATURES.PHYSICAL_PRODUCT_BUILDER, ] if not any(feature in vendor_attributes['features'] for feature in required_features): return None return TenantApplication( application_id=app_model.WORKSTATION_CATALOG_APP, roles=[app_model.WORKSTATION_CATALOG_ROLE], url=app_model.get_application_url( app_model.WORKSTATION_CATALOG_APP, vendor_attributes['company_brand']['name'] ), ) def get_collaborators_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get Collaborators application for a vendor based on the vendor's attributes. Args: vendor_attributes (TenantApplication): Vendor attributes. Return: TenantApplication | None: Collaborators application with a base role. """ company_brand_name = vendor_attributes['company_brand']['name'] has_feature = ( company_brand_name in ['ab', 'hrs', 'msk', 'theorchard', 'sme'] and FEATURES.ACCOUNTING in vendor_attributes['features'] ) # Unfortunately turning on accounting FC for these would break stuff # We don't have a way of indicating "yes collaborators but no accounting" if not has_feature and company_brand_name not in ['awal', 'altafonte']: return None return TenantApplication( application_id=app_model.COLLABORATORS_APP, roles=[app_model.COLLABORATORS_BASE_ROLE], url=app_model.get_application_url( app_model.COLLABORATORS_APP, vendor_attributes['company_brand']['name'] ), ) def get_banking_and_tax_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get Banking and Tax application for a vendor based on the vendor's attributes. Args: vendor_attributes (TenantApplication): Vendor attributes. Return: TenantApplication | None: Banking and Tax application with a base role. """ if not vendor_attributes['migrated_to_abacus']: return None if vendor_attributes['company_brand']['name'] in ['knr']: return None return TenantApplication( application_id=app_model.BANKING_TAX_APP, roles=[app_model.BANKING_TAX_BASE_ROLE], url=app_model.get_application_url( app_model.BANKING_TAX_APP, vendor_attributes['company_brand']['name'] ), ) def get_settings_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication: """ Get settings application for a vendor based on the vendor's attributes. Args: vendor_attributes (TenantApplication): Vendor attributes. Return: TenantApplication: Settings """ roles = [app_model.SETTINGS_BASE_ROLE] return TenantApplication( application_id=app_model.SETTINGS_APP, roles=roles, url=app_model.get_application_url( app_model.SETTINGS_APP, vendor_attributes['company_brand']['name'] ), ) def get_fansifter_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get fansifter application for a vendor based on the vendor's attributes. Args: vendor_attributes (TenantApplication): Vendor attributes. Return: TenantApplication: Fansifter """ has_feature = FEATURES.FANSIFTER in vendor_attributes['features'] if not has_feature: return None roles = [app_model.FANSIFTER_CAN_VIEW_FAN_DATA] if FEATURES.FANSIFTER_AD_REPORTING in vendor_attributes['features']: roles.append(app_model.FANSIFTER_CAN_CREATE_AD_REPORTS) if FEATURES.FANSIFTER_AD_CAMPAIGN_AUDIENCES in vendor_attributes['features']: roles.append(app_model.FANSIFTER_CAN_SHARE_AD_CAMPAIGN_AUDIENCES) roles.append(app_model.FANSIFTER_CAN_CONNECT_AD_ACCOUNTS) if FEATURES.FANSIFTER_EMAIL_CAMPAIGNS in vendor_attributes['features']: roles.append(app_model.FANSIFTER_CAN_VIEW_EMAIL_CAMPAIGNS) roles.append(app_model.FANSIFTER_CAN_CREATE_EMAIL_CAMPAIGNS) if FEATURES.FANSIFTER_SMS_CAMPAIGNS in vendor_attributes['features']: roles.append(app_model.FANSIFTER_CAN_VIEW_SMS_CAMPAIGNS) roles.append(app_model.FANSIFTER_CAN_CREATE_SMS_CAMPAIGNS) return TenantApplication( application_id=app_model.FANSIFTER_APP, roles=roles, url=app_model.get_application_url( app_model.FANSIFTER_APP, vendor_attributes['company_brand']['name'] ), ) def get_songwhip_app_for_vendor( vendor_attributes: types.VendorAttributes, ) -> types.TenantApplication | None: """ Get songwhip application for a vendor based on the vendor's attributes. Args: vendor_attributes (TenantApplication): Vendor attributes. Return: TenantApplication | None: Songwhip application with a read role. """ songwhip_enabled = pythonfeatures.get_single_feature( constants.FEATURE_SONGWHIP_IN_SETTINGS_V2, g.request_context ) if not songwhip_enabled.message == split_constants.FEATURE_ENABLED: return None has_feature = FEATURES.SONGWHIP in vendor_attributes['features'] if not has_feature: return None return TenantApplication( application_id=app_model.SONGWHIP_APP, roles=[app_model.SONGWHIP_READ_ROLE], url=app_model.get_application_url( app_model.SONGWHIP_APP, vendor_attributes['company_brand']['name'] ), ) def generate_vendor_app_check_map( vendor_attributes: types.VendorAttributes, ) -> dict[str, VendorApplicationChecker]: """ Generates a dictionary of functions that can be used to check if a particular application is enabled for a given vendor. The list of applications to check can become dynamic in the future. Args: vendor_attributes (VendorAttributes): The vendor object used to check enabled applications. Returns: dict[str, Callable[[], TenantApplication]]]: A dictionary where keys are application IDs and values are functions that check the enabled status of those applications. """ app_checkers: dict[str, VendorApplicationChecker] = { app_model.BANKING_TAX_APP: get_banking_and_tax_app_for_vendor, app_model.COLLABORATORS_APP: get_collaborators_app_for_vendor, app_model.CUSTOMER_ACCOUNTING_APP: get_customer_accounting_app_for_vendor, app_model.INSIGHTS_APP: get_insights_app_for_vendor, app_model.FANSIFTER_APP: get_fansifter_app_for_vendor, app_model.SONGWHIP_APP: get_songwhip_app_for_vendor, app_model.WORKSTATION_ANALYTICS_APP: get_ws_analytics_app_for_vendor, app_model.WORKSTATION_CATALOG_APP: get_ws_catalog_app_for_vendor, app_model.WORKSTATION_MARKETING_APP: get_ws_marketing_app_for_vendor, app_model.SETTINGS_APP: get_settings_app_for_vendor, } return { app: (lambda app_id=app: app_checkers[app_id](vendor_attributes)) for app in ENABLED_APPLICATIONS } def _has_workstation_apps(applications: list[types.TenantApplication]) -> bool: """ Check if any workstation apps are present in the applications list. Args: applications: List of enabled applications to check. Returns: bool: True if any workstation apps are present, False otherwise. """ workstation_app_ids = { app_model.WORKSTATION_ANALYTICS_APP, app_model.WORKSTATION_CATALOG_APP, app_model.WORKSTATION_MARKETING_APP, } return any(app['application_id'] in workstation_app_ids for app in applications) def get_enabled_vendor_applications( vendor_attributes: types.VendorAttributes, ) -> list[types.TenantApplication]: """ Get all applications for a vendor. Args: vendor_attributes (VendorAttributes): The vendor object used to check enabled applications. Return: list[TenantApplication]: list of vendor applications. """ # Short-circuit for 3rd Party Payee Accounts: only return accounting app if enabled owner = vendor_attributes.get('owner') if owner == features.THIRD_PARTY_PAYEE_OWNER: if FEATURES.ACCOUNTING in vendor_attributes['features']: accounting_app = get_customer_accounting_app_for_vendor(vendor_attributes) banking_tax_app = get_banking_and_tax_app_for_vendor(vendor_attributes) return [app for app in [accounting_app, banking_tax_app] if app is not None] return [] applications_map = generate_vendor_app_check_map(vendor_attributes) applications = [func() for app_id, func in applications_map.items()] enabled_applications = [app for app in applications if app is not None] # Check if any workstation apps are enabled and add WORKSTATION_ADMIN_ROLE to Settings app settings_app = next( (app for app in enabled_applications if app['application_id'] == app_model.SETTINGS_APP), None, ) if settings_app and _has_workstation_apps(enabled_applications): settings_app['roles'].append(app_model.WORKSTATION_ADMIN_ROLE) return enabled_applications @db_read_only_session_wrap def get_vendor_applications(vendor_uuid: str, session: Session) -> response.Response: """ Get all applications for a vendor. Args: vendor_uuid (str): Vendor identifier. session (Session): SQLAlchemy session. Return: Response: containing dict of applications and roles """ result = vendor_model.lookup_vendor_and_company_brand_by_uuid(vendor_uuid, session=session) if result is None: return response.create_not_found_response(message='Vendor not found') vendor, company_brand = result fc = feature_model.get_enabled_features_for_vendor_with_session( vendor['vendor_id'], session=session ) vendor_features = feature_model.convert_features_to_list(fc) vendor_attributes = types.VendorAttributes( vendor_id=vendor['vendor_id'], uuid=vendor['uuid'], migrated_to_abacus=vendor['migrated_to_abacus'], company_brand=company_brand, features=vendor_features, owner=vendor.get('owner'), ) applications = get_enabled_vendor_applications(vendor_attributes) return response.Response({'applications': applications}) @db_read_only_session_wrap def get_bulk_vendor_applications( vendor_uuids: list[uuid.UUID], session: Session ) -> dict[str, list[dict]]: """ Get all applications for multiple vendors. Args: vendor_uuids (list[uuid.UUID]): List of vendor identifiers. session (Session): SQLAlchemy session. Return: Response: containing dict mapping vendor UUIDs to their applications and roles """ # Bulk lookup vendors and company brands vendor_uuids_str = [str(uuid) for uuid in vendor_uuids] vendors_lookup = vendor_model.lookup_vendors_and_company_brands_by_uuids( vendor_uuids=vendor_uuids_str, session=session ) # Extract vendor IDs for bulk features lookup vendor_ids = [vendor_data[0]['vendor_id'] for vendor_data in vendors_lookup.values()] # Bulk lookup features for all valid vendor IDs features_lookup = feature_model.get_enabled_features_for_vendors_with_session( vendor_ids=vendor_ids, session=session ) # Combine results into VendorAttributes list (intersection of vendors with features) vendor_attributes_list = [] for vendor_uuid, vendor_data in vendors_lookup.items(): vendor, company_brand = vendor_data vendor_id = vendor['vendor_id'] # Only include vendors that have features features = features_lookup.get(vendor_id) if features is not None: vendor_features = feature_model.convert_features_to_list(features) vendor_attributes = types.VendorAttributes( vendor_id=vendor['vendor_id'], uuid=vendor['uuid'], migrated_to_abacus=vendor['migrated_to_abacus'], company_brand=company_brand, features=vendor_features, owner=vendor.get('owner'), ) applications = get_enabled_vendor_applications(vendor_attributes) vendor_attributes_list.append({'uuid': vendor_uuid, 'applications': applications}) # format_for_dataloader to handle ordering and None entries return { 'vendors': format_for_dataloader( items=vendor_attributes_list, keys=vendor_uuids_str, lookup_key='uuid' ) }