"""Lambda generate_attachments function module.""" import json import os from os import path import shutil from lambdacommon.common_config import logger import sentry_sdk from sentry_sdk import capture_exception from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration import config from src.connectors import s3 from src.connectors.ows_moneyhub import OwsMoneyhub from src.documents.collection_summary import build_document as build_collection_summary from src.documents.distribution_fee_invoice import build_document as build_distribution_fee_document from src.documents.legacy_revenue_report_pandas import ( build_full_document as build_legacy_revenue_report_full, ) from src.documents.legacy_revenue_report_pandas import ( build_physical_document as build_legacy_revenue_report_physical, ) from src.documents.neighbouring_rights_label_revenue import ( build_document as build_neighbouring_rights_label_revenue, ) from src.documents.neighbouring_rights_performer_revenue import ( build_document as build_neighbouring_rights_performer_revenue, ) from src.documents.revenue_detail_report import build_document as build_revenue_detail_report from src.documents.self_billing_invoice import build_document as build_self_billing_document from src.utils.constants import StatementAttachmentFailureReason from src.utils.constants import StatementAttachmentStatus from src.utils.constants import StatementAttachmentType from src.utils.dataclasses import StatementAttachmentPayload from src.utils.error_handling import ClientException from src.utils.error_handling import LambdaException SYSTEM_ERROR = StatementAttachmentFailureReason.SYSTEM_ERROR if config.SENTRY_DSN: sentry_sdk.init( dsn=config.SENTRY_DSN, ignore_errors=[ 'Payee tax details not found', ], environment=config.ENVIRONMENT, integrations=[AwsLambdaIntegration(timeout_warning=True)], ) def _generate_attachment_file(statement_attachment: dict) -> str: """Generate an attachment file based on its type. Args: statement_attachment (dict): Attachment to generate """ attachment_type = statement_attachment['statement_attachment_type'] match attachment_type: case StatementAttachmentType.DISTRIBUTION_FEE: local_path = build_distribution_fee_document(statement_attachment) case StatementAttachmentType.NEIGHBOURING_RIGHTS_LABEL_REVENUE: local_path = build_neighbouring_rights_label_revenue( statement_attachment['account_id'], statement_attachment['contract_id'], statement_attachment['statement_period_id'], ) case StatementAttachmentType.NEIGHBOURING_RIGHTS_PERFORMER_REVENUE: local_path = build_neighbouring_rights_performer_revenue( statement_attachment['account_id'], statement_attachment['contract_id'], statement_attachment['statement_period_id'], ) case StatementAttachmentType.REVENUE_DETAIL: local_path = build_revenue_detail_report( statement_attachment['account_id'], statement_attachment['contract_id'], statement_attachment['statement_period_id'], statement_attachment['subaccount_id'], statement_attachment['file_type'], statement_attachment['number_format'], ) case StatementAttachmentType.LEGACY_REVENUE_DETAIL_FULL: local_path = build_legacy_revenue_report_full( statement_attachment['account_id'], statement_attachment['statement_period_ids'] if statement_attachment['statement_period_ids'] else str(statement_attachment['statement_period_id']), # noqa:E501 statement_attachment['subaccount_id'], statement_attachment['file_type'], statement_attachment['number_format'], statement_attachment.get('filters'), ) case StatementAttachmentType.LEGACY_REVENUE_DETAIL_PHYSICAL: local_path = build_legacy_revenue_report_physical( statement_attachment['account_id'], statement_attachment['statement_period_ids'] if statement_attachment['statement_period_ids'] else str(statement_attachment['statement_period_id']), # noqa:E501 statement_attachment['subaccount_id'], statement_attachment['file_type'], statement_attachment['number_format'], statement_attachment.get('filters'), ) case StatementAttachmentType.SELF_BILLING: local_path = build_self_billing_document(statement_attachment) case StatementAttachmentType.COLLECTION_SUMMARY_LABEL: local_path = build_collection_summary(statement_attachment) case StatementAttachmentType.COLLECTION_SUMMARY_PERFORMER: local_path = build_collection_summary(statement_attachment) case _: raise LambdaException(f'Unhandled statement attachment type: {attachment_type}') return local_path def _generate_attachment(statement_attachment: dict) -> None: """Generate a statement attachment. Args: statement_attachment (dict): The attachment to generate """ try: # Generate document logger.info( f'Generating statement attachment {statement_attachment["statement_attachment_id"]} ' f'({statement_attachment["statement_attachment_type"]})' ) local_path = _generate_attachment_file(statement_attachment) logger.info(f'File created: {local_path}') if config.ENVIRONMENT == config.DEV_ENVIRONMENT: return # Upload to S3 s3_object_path = path.join( str(statement_attachment['account_id']), str(statement_attachment['statement_period_id']), str(statement_attachment['statement_attachment_id']), path.relpath(local_path, config.FILE_OUTPUT_PATH), ) full_s3_path = s3.full_path(s3_object_path) logger.info(f'Uploading document to S3: {full_s3_path}') s3.upload_file(local_path, s3_object_path) # Update statement attachment data = StatementAttachmentPayload( file_location=full_s3_path, statement_attachment_status=StatementAttachmentStatus.COMPLETE, ) logger.info( f'Updating database entry ({statement_attachment["statement_attachment_id"]}): {data}' ) OwsMoneyhub.update_statement_attachment( statement_attachment['statement_attachment_id'], data ) # Remove local file logger.info('Removing local file...') os.remove(local_path) except Exception as e: logger.exception(str(e)) reason = e.failure_reason if hasattr(e, 'failure_reason') else SYSTEM_ERROR payload = StatementAttachmentPayload( statement_attachment_status=StatementAttachmentStatus.ERROR, failure_reason=reason ) OwsMoneyhub.update_statement_attachment( statement_attachment['statement_attachment_id'], payload ) raise e def _generate_attachment_by_id(statement_attachment_id: int) -> None: """Generate a statement attachment fetched by ID. Args: statement_attachment_id (int): The attachment to generate """ statement_attachment = OwsMoneyhub.get_statement_attachment(statement_attachment_id) _generate_attachment(statement_attachment) def _generate_attachments_by_account( account_id: int, statement_period_id: int, subaccount_id: int | None ) -> None: """Generate attachments based on an account/period (and optional subaccount). Args: account_id (int): Account to fetch attachments for statement_period_id (int): Period to fetch attachments for subaccount_id (int): Subaccount to filter attachments by """ # We loop through multiple attachments, so hold exception raising to the end exceptions = [] statement_attachments = [ attachment for attachment in OwsMoneyhub.get_statement_attachments( account_id, statement_period_id, subaccount_id ) if attachment['statement_attachment_status'] != StatementAttachmentStatus.COMPLETE and attachment['statement_attachment_type'] != StatementAttachmentType.INTERNAL_UPLOAD ] # noqa: E501 logger.info(f'statement_attachments={statement_attachments}') for statement_attachment in statement_attachments: try: _generate_attachment(statement_attachment) except Exception as e: exceptions.append(e) # If any exceptions occured, raise the first to retrigger the lambda if len(exceptions): raise exceptions[0] def handler(event: dict, context: dict | None = None) -> None: """Lambda entry point. Args: event (dict): Contains the event data context (LambdaContext): The context with which the lambda was run Raises: LambdaException: Missing account_id in event payload. LambdaException: Missing statement_period_id in event payload. LambdaException: Missing Account Payment Term for the specified account_id. OwsException: Missing tax information """ try: logger.info(f'event={event}') if 'Records' in event: payloads = [json.loads(record['body']) for record in event['Records']] else: payloads = [event] for payload in payloads: if 'statement_attachment_id' in payload: _generate_attachment_by_id(int(payload['statement_attachment_id'])) continue if 'account_id' not in payload: raise ClientException('Missing account_id in event payload') if 'statement_period_id' not in payload: raise ClientException('Missing statement_period_id in event payload') subaccount_id = payload.get('subaccount_id') _generate_attachments_by_account( int(payload['account_id']), int(payload['statement_period_id']), int(subaccount_id) if subaccount_id else None, ) except Exception as e: capture_exception(e) raise e finally: # Clear contents of /tmp directory if config.ENVIRONMENT not in [config.DEV_ENVIRONMENT, config.TEST_ENVIRONMENT]: try: tmp_dir = '/tmp' if os.path.exists(tmp_dir): for filename in os.listdir(tmp_dir): file_path = os.path.join(tmp_dir, filename) try: if os.path.isfile(file_path) or os.path.islink(file_path): os.unlink(file_path) elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as cleanup_error: logger.warning(f'Failed to delete {file_path}: {cleanup_error}') capture_exception(cleanup_error) raise cleanup_error logger.info('Cleared contents of /tmp directory') except Exception as e: logger.warning(f'Failed to clear /tmp directory: {e}') capture_exception(e) raise e