"""Logic for Statement Period Adjustment File.""" import typing from decimal import Decimal from abacus_common_logic.adjustments_validation import ( Adjustment, AdjustmentsValidationSnowflakeExecutor, SnowflakeConfig, validate_adjustments as validate_adjustments_common, ) from marshmallow import ValidationError from owsresponse import response from core.config import Config from royalties import models, schemas from royalties.connectors.s3 import create_presigned_url, get_s3_client from royalties.constants import constants, error from royalties.utils.aws import parse_s3_url from royalties.utils.format_error import validation_error def get_adjustments_template_xlsx(): """Generate presigned S3 url to download adjustments template xlsx.""" bucket = Config.S3_ABACUS_ADJUSTMENTS_BUCKET client = get_s3_client() key = 'abacus_adjustments_template/Adjustment_Template.xlsx' res = create_presigned_url(client, bucket, key) return response.Response(message=res, status=200) def get_adjustment_file_invalid_report(statement_period_adjustment_file_id: int): """Generate presigned S3 url to download adjustment error report. Args: statement_period_adjustment_file_id (id): id of the adjustment Returns: a presigned S3 url """ adjustment_file = models.StatementPeriodAdjustmentFile.get_by_id_or_error( statement_period_adjustment_file_id ) invalid_file_url = adjustment_file.invalid_file_location try: bucket, key = parse_s3_url(invalid_file_url) client = get_s3_client() res = create_presigned_url(client, bucket, key) return response.Response(message=res, status=200) except Exception: return None def get_adjustment_file_valid_report(statement_period_adjustment_file_id: int): """Generate presigned S3 url to download adjustment valid report. Args: statement_period_adjustment_file_id (id): id of the adjustment Returns: a presigned S3 url """ adjustment_file = models.StatementPeriodAdjustmentFile.get_by_id_or_error( statement_period_adjustment_file_id ) valid_file_url = adjustment_file.valid_file_location try: bucket, key = parse_s3_url(valid_file_url) client = get_s3_client() res = create_presigned_url(client, bucket, key) return response.Response(message=res, status=200) except Exception: return None def create_statement_period_adjustment_file( statement_period_id: int, file_name: str, batch_type: str = None, valid_file_location: typing.Optional[str] = None, invalid_file_location: typing.Optional[str] = None, valid_row_count: typing.Optional[int] = None, invalid_row_count: typing.Optional[int] = None, total_file_amount_multicurrency: typing.Optional[Decimal] = None, total_rounded_amount_multicurrency: typing.Optional[Decimal] = None, md5sum: typing.Optional[str] = None, error_type: typing.Optional[str] = None, source_file_upload_id: typing.Optional[int] = None, created_by: typing.Optional[str] = None, ) -> response.Response: """Create statement period adjustment file logic. Args: batch_type (str): Batch creation type - auto (generated), manual (UI entry), upload (file upload) statement_period_id (int): ID of the accounting_period adjustment_file file_name (str): name of the adjustment_file valid_file_location (str): location of the valid file invalid_file_location (str): location of the invalid file valid_row_count (int): valid rows count invalid_row_count (int): invalid rows count total_file_amount_multicurrency (decimal.Decimal): total file amount multicurrency total_rounded_amount_multicurrency (decimal.Decimal): total rounded amount multicurrency md5sum (str): md sum of the file error_type (str): error type source_file_upload_id (int): id of the source file upload created_by (str): identity id of the user who uploaded the adjustment_file Returns: an ows response """ try: _validate_statement_period_state(statement_period_id) except Exception as e: return validation_error(str(e)) file_data = dict( statement_period_id=statement_period_id, file_name=file_name, valid_file_location=valid_file_location, invalid_file_location=invalid_file_location, valid_row_count=valid_row_count, invalid_row_count=invalid_row_count, total_file_amount_multicurrency=total_file_amount_multicurrency, total_rounded_amount_multicurrency=total_rounded_amount_multicurrency, md5sum=md5sum, error_type=error_type, source_file_upload_id=source_file_upload_id, ) if batch_type is not None: file_data['batch_type'] = batch_type if created_by is not None: file_data['created_by'] = created_by new_adjustment_file = models.StatementPeriodAdjustmentFile.create(**file_data) return response.Response( message=schemas.StatementPeriodAdjustmentFileDetailSchema().dump( new_adjustment_file ), status=201, ) def update_statement_period_adjustment_file( statement_period_adjustment_file: models.StatementPeriodAdjustmentFile, **params: dict, ) -> response.Response: """Update statement period adjustment file. Args: statement_period_adjustment_file (models.StatementPeriodAdjustmentFile): object to update params (dict): dict of params to update Example: { 'file_name': 'test_file.csv', 'valid_file_location': 'valid/location', 'invalid_file_location': 'invalid/location', 'valid_row_count': 1, 'invalid_row_count': 2, 'total_file_amount_multicurrency': Decimal('3.000000000001'), 'total_rounded_amount_multicurrency': Decimal('4.02'), 'md5sum': 'b6579ec2950296ed6a04f08f67f64422', 'error_type: 'content_error', 'source_file_upload_id': 1 } Returns: an ows response """ try: _validate_statement_period_state( statement_period_adjustment_file.statement_period_id ) except Exception as e: return validation_error(str(e)) statement_period_adjustment_file.update_attributes(**params) statement_period_adjustment_file.commit_changes() return response.Response( message=schemas.StatementPeriodAdjustmentFileDetailSchema().dump( statement_period_adjustment_file ), status=201, ) def delete_statement_period_adjustment_file( statement_period_adjustment_file: models.StatementPeriodAdjustmentFile, ) -> response.Response: """Delete statement period adjustment file. Args: statement_period_adjustment_file (models.StatementPeriodAdjustmentFile): object to delete Returns: an ows response """ try: _validate_statement_period_state( statement_period_adjustment_file.statement_period_id ) except Exception as e: return validation_error(str(e)) models.StatementPeriodAdjustmentFile.delete_by_id_or_error( statement_period_adjustment_file.statement_period_adjustment_file_id, soft_delete=True, ) return response.Response(status=204) def _validate_statement_period_state(statement_period_id): """Validate statement period.""" statement_period = models.StatementPeriod.get_by_id_or_error(statement_period_id) if ( statement_period.statement_period_status != constants.STATEMENT_PERIOD_STATUSES.CURRENT ): raise Exception(error.ERROR_NO_CURRENT_STATEMENT_PERIOD) def get_adjustment_file_by_source_file_key(source_file_key: str): """Fetch a StatementPeriodAdjustmentFile by source_file_key. Args: source_file_key (str): file_key of the source file upload. Returns: response.Response: The response containing the adjustment file or an error. """ adjustment_file = models.StatementPeriodAdjustmentFile.get_by_source_file_key( source_file_key ) if not adjustment_file: return response.create_error_response( code='adjustment_file_not_found', message=f'No adjustment file found for file_key: {source_file_key}', status=404, ) return response.Response( message=schemas.StatementPeriodAdjustmentFileDetailSchema().dump( adjustment_file ), status=200, ) def get_statement_period_adjustment_files(request_params: dict) -> response.Response: """Get a list of statement_period_adjustment_file. Args: request_params (dict)(Optional): query string parameters - limit (int): the size of page - offset (int): the number of items to skip before returning results - sort_by (str): column name by which the results should be sorted - sort_order (str): order direction (asc or desc) - statement_period_adjustment_file_id (int): id of the statement_period_adjustment_file - statement_period_id (int): id of the statement_period - status (str): status of the adjustment file can either be approved, not_approved, or applied - created_by (str): identity id or name of the user who uploaded the statement_period_adjustment_file Returns: a list of statement period adjustment files. """ try: params = schemas.StatementPeriodAdjustmentFileFilterSchema().load( request_params ) items, total_count = ( models.StatementPeriodAdjustmentFile.get_statement_period_adjustment_files( **params ) ) message = dict( items=schemas.StatementPeriodAdjustmentFileListSchema().dump( items, many=True ), total_count=total_count, ) except ValidationError as exc: return validation_error(str(exc)) except Exception as e: raise e return response.Response(message=message, status=200) def get_statement_period_adjustment_file_users(user_action: str): """Get the list of users by user action. Args: user_action (str): can be "uploaded-file" Returns: a list of users. """ adjustment_file_user_actions = ( constants.STATEMENT_PERIOD_ADJUSTMENT_FILE_USER_ACTIONS ) try: if user_action not in adjustment_file_user_actions: raise ValidationError( error.ERROR_INVALID_ADJUSTMENT_FILE_USER_ACTION.format( ', '.join(adjustment_file_user_actions) ) ) items, total_count = ( models.StatementPeriodAdjustmentFile.get_statement_period_adjustment_file_users( user_action ) ) message = dict( items=schemas.StatementPeriodAdjustmentFileUserListSchema().dump( items, many=True ), total_count=total_count, ) return response.Response(message=message, status=200) except ValidationError as e: return validation_error(str(e)) def validate_adjustments(adjustments: list[Adjustment], statement_period_id: int): """Validate a list of manual adjustments. Args: adjustments: The list of adjustments to validate. statement_period_id: The ID of the current statement period. Returns: A Response object with the validation errors. """ with AdjustmentsValidationSnowflakeExecutor( typing.cast(SnowflakeConfig, Config.SNOWFLAKE_CONFIG) ) as sf_executor: errors = validate_adjustments_common( adjustments, statement_period_id, sf_executor ) results = {} for key, error_set in errors.items(): results[key] = list(error_set) return response.Response(message=results, status=200) def get_in_progress_auto_generated_adjustments( statement_period_id: int, identity_id: str ) -> response.Response: """Get the auto-generated adjustment file that are currently being processed or have encountered an error. Args: statement_period_id (int): id of the statement period identity_id (str): identity id of the individual who generated the adjustments Returns: an OWS response """ try: _validate_statement_period_state(statement_period_id) except Exception as e: return validation_error(str(e)) adjustment_file = ( models.StatementPeriodAdjustmentFile.get_in_progress_auto_generated_adjustments( statement_period_id, identity_id ) ) result = schemas.AutoGenerationInProgressOrErrorSchema(many=True).dump( adjustment_file ) return response.Response(message=result, status=200)