"""Lambda function module for create_project.""" from typing import Dict, Optional import uuid from common.models.state_machine.grps_ingestion_context import \ GrpsIngestionContext from common.schemas.state_machine_schema import StateMachineSchema from lambdacommon.graphql import graphql import config from config import graphql_gateway from src.constants.queries import CREATE_PROJECT from src.constants.queries import GET_PRODUCT_BY_UPC from src.constants.queries import GET_PROJECT_BY_PROJECT_CODE from src.exceptions import ArtistIdNotFoundException from src.exceptions import CreateProjectException from src.exceptions import DuplicateProjectCodeException logger = config.app_logger def handler(event, context): """Create project handler. 1. Checks whether the UPC already has a different project code associated to it (ProjectCodeMismatchException for non-SME providers). 2. Checks whether the project already exists by project code. 3. Creates the project if it does not exist. 4. Never updates an existing project. """ logger.info(f'Triggered create_project: {event}') sm_context = StateMachineSchema().load(event) correlation_id = sm_context.correlation_id or str(uuid.uuid4()) graphql_gateway.set_headers( { 'Orchard-User-Id': config.OA_USER, 'Correlation-Id': correlation_id, } ) check_project_code_mismatch(sm_context) try: graphql_result = check_for_project(sm_context) if graphql_result: project_id = graphql_result.get('projectId') logger.info( f'Project already exists with project_code: ' f'{sm_context.project.project_code} ' f'and project_id: {project_id}' ) sm_context.project.project_id = project_id else: result = create_project(sm_context) sm_context.project.project_id = result.get('projectId') except graphql.GraphQLError as err: message = str(err) logger.error(f'GraphQL error in create_project: {message}') raise CreateProjectException(message) from err return StateMachineSchema().dump(sm_context) def check_project_code_mismatch(context: GrpsIngestionContext) -> None: """Check whether the UPC is already linked to a different project code. Logs a warning when the project code on the existing product does not match the one in the context, but does not raise. Args: context: State machine context containing product and project info. """ upc = context.product.upc if context.product else None if not upc: logger.info('No UPC found, skipped check_project_code_mismatch') return context_project_code = ( context.project.project_code if context.project else None ) result = graphql_gateway.execute( GET_PRODUCT_BY_UPC, {'upc': upc} )['data']['productByUpc'] logger.info( f'Received product for upc {upc}: {result}' ) if not result: return graphql_project_code: Optional[str] = ( result.get('project', {}) or {} ).get('projectCode') # Normalise SONY prefix if context_project_code: context_project_code = context_project_code.replace('SONY:id:', '') if graphql_project_code: graphql_project_code = graphql_project_code.replace('SONY:id:', '') if graphql_project_code and graphql_project_code != context_project_code: logger.warning( f'GraphQL project code "{graphql_project_code}" does not match ' f'context project code "{context_project_code}" for upc {upc}' ) def check_for_project(context: GrpsIngestionContext) -> Dict: """Check for the existence of a project by project code. Args: context: State machine context containing product and project info. Returns: dict: Project data if found, empty dict otherwise. """ project_code = ( context.project.project_code if context.project else None ) if not project_code: logger.info('No project_code found, skipped check_for_project') return {} # GraphQL requires subaccount_id to be 0 when null vendor_id = context.product.vendor_id subaccount_id = context.product.subaccount_id or 0 logger.info( f'Checking for existing project with project_code: {project_code}' f', vendor_id: {vendor_id}, subaccount_id: {subaccount_id}' ) payload = { 'projectCode': project_code, 'accountId': vendor_id, 'subaccountId': subaccount_id, } result = graphql_gateway.execute( GET_PROJECT_BY_PROJECT_CODE, payload )['data']['projectByProjectCode'] logger.info(f'Get project response: {result}') return result or {} def create_project(context: GrpsIngestionContext) -> Dict: """Create a project from context data. Args: context: State machine context containing product and project info. Returns: dict: Created project data from GraphQL response. Raises: DuplicateProjectCodeException: When the project code already exists (race condition between concurrent executions). """ vendor_id = context.product.vendor_id subaccount_id = context.product.subaccount_id or 0 project_code = ( context.project.project_code if context.project else None ) or str(uuid.uuid4())[:10] project_name = ( context.project.name if context.project else None ) or str(uuid.uuid4())[:10] logger.info( f'Running create_project with vendor_id: {vendor_id}' f', subaccount_id: {subaccount_id}' f', project_code: {project_code}' ) payload = { 'data': { 'projectCode': project_code, 'name': project_name, 'accountId': vendor_id, 'subaccountId': subaccount_id, 'artistId': _retrieve_artist_id(context) } } try: result = graphql_gateway.execute( CREATE_PROJECT, payload )['data']['createProject'] except graphql.GraphQLError as err: # Race condition: project was created by another concurrent execution # after we checked. Re-running the lambda will find the project on the # next attempt. if f"Project code '{project_code}' already exists" in str(err): raise DuplicateProjectCodeException(err) from err raise if result: logger.info( f'Project created with project_code: {project_code}, ' f'project_id: {result.get("projectId")}' ) return result or {} def _retrieve_artist_id( context: GrpsIngestionContext) -> Optional[int]: """Match project artist name to a label participant to get artist_id. Resolves artistId by matching project.artist.name against label_participants[].name and returning label_participant.artist_id. Returns None when label_participants is absent or no match is found. Args: context: State machine context. Returns: Integer artist_id if resolved, None otherwise. """ artist = context.project.artist if context.project else None label_participants = context.label_participants or [] if not artist or not label_participants: return None for lp in label_participants: if lp.name == artist.name and lp.artist_id: return int(lp.artist_id) raise ArtistIdNotFoundException( f'Artist id not found for name: {artist.name}' )