"""Blueprint for payee's payment information.""" import datetime from http import HTTPStatus import io import logging from abacus_common_logic.views.create_view import CreateView import analytics from common_apispec import doc, marshal_with, use_kwargs from flask import abort, Blueprint, current_app, g, request, send_file import jwt from marshmallow import EXCLUDE from owsresponse import response from owsresponse.adaptors.flask import flaskify from payee.config import Config from payee.constants import error from payee.constants.constants import ( PERMISSIONS_RESOURCE_TYPES, PROFILES, ) from payee.logic.payoneer import ( generate_payee_payoneer_session_id, get_payee_corp_entity, get_payoneer_account, ) from payee.logic.secure_document import ( create_secure_document_details, dataload_secure_document_details, get_secure_document_details, get_secure_documents_list_response, has_secure_document_details, validate_dataload_request, ) from payee.logic.tax_details import ( build_tax_details_response, create_tax_details, get_tax_details, get_tax_receipt_details, has_tax_details, resync_payment_readiness, ) from payee.models.knr_details import KnrDetailsDocument from payee.models.tax_details import TaxDetailsDocument from payee.schemas.tax_details import ( get_tax_details_schema_for_corp_entity, KnrDetailsSchema, ValidateTaxDetailsParamsSchema, ) from payee.utils.aws import get_s3_url_for_file from payee.utils.format_data import flatten_input_data from payee.utils.pdf_generation import generate_pdf from payee.utils.permissions import ( check_access, check_jwt_identity, check_record_owner, or_, ) from payee.utils.validations import validate_multiple_record_owner tax_details_api = Blueprint('tax_details_api', __name__) logger = logging.getLogger('segment_logger') class PayeeTaxDetailsCreateView(CreateView): """POST secure payment-info for a specified payee.""" def post(self, **kwargs): """Extend base post method.""" # what corp entity is the user signed to? # this should be based on account lookup corp_entity = get_payee_corp_entity(kwargs['account_payee_id']) schema = get_tax_details_schema_for_corp_entity(corp_entity) if not schema: error_message = response.create_error_response( 'error', message=error.ERROR_INVALID_CORP_ENTITY.format(corp_entity=corp_entity), ) return flaskify(error_message) self.post_schema = schema() return super().post(**kwargs) @or_( check_jwt_identity(Config.TAX_FORM_SERVICE_IDENTITIES), check_record_owner(), wrap_response=True, ) def create_handler(self, **params): """Create tax-details.""" # Preprocess input data to make it single-level dict flatten_input_data(params) account_payee_id = params['account_payee_id'] result = create_tax_details(current_app.config.get('SDM_CONFIG'), **params) try: analytics.identify(account_payee_id) analytics.track( account_payee_id, 'Tax Details submitted', { 'account_payee_id': account_payee_id, }, ) g.log.info( f'Message with topic `Tax Details submitted`' f' for account_payee_id:{account_payee_id} was sent.' ) except Exception as e: g.log.error(f'Error when sending message to segment:`{str(e)}`') return response.Response(message=result, status=201) @tax_details_api.route( '/account-payee//tax-details', methods=['GET'] ) @or_( check_jwt_identity(Config.TAX_FORM_SERVICE_IDENTITIES), check_record_owner(), ) def tax_details(account_payee_id): """Endpoint to GET secure tax-details for a specified payee.""" try: tax_document = has_tax_details(account_payee_id) if not tax_document: return flaskify( response.Response( message=error.ERROR_PAYEE_TAX_DETAILS_NOT_FOUND.format( account_payee_id=account_payee_id ), status=404, ) ) message = build_tax_details_response(account_payee_id, tax_document) return flaskify(response.Response(message=message, status=200)) except Exception as err: message = f'Unexpected {err=}, {type(err)=}' return flaskify(response.Response(message=message, status=500)) tax_details_api.add_url_rule( '/account-payee//tax-details', view_func=PayeeTaxDetailsCreateView.as_view('create_tax_details'), methods=['POST'], ) @tax_details_api.route( '/account-payee//tax-details/receipt', methods=['GET'] ) def generate_receipt(account_payee_id): """Endpoint to generate a PDF of the tax info.""" result = validate_multiple_record_owner([account_payee_id])[account_payee_id] if not result: return flaskify( response.Response( message={'error': error.ERROR_DONT_HAVE_PERMISSIONS}, status=401 ) ) params = get_tax_receipt_details(account_payee_id) pdf_bytes = generate_pdf(params) return send_file(io.BytesIO(pdf_bytes), mimetype='application/pdf') @tax_details_api.route( '/account-payee//tax-details/receipt-url', methods=['GET'] ) def generate_receipt_url(account_payee_id): """Endpoint to generate a PDF of the tax info, upload it to S3 and return a URL. This endpoint uploads a file to S3 every time it is called, we need to make sure that the path where these files are stored has a short expiration time to prevent a large number of temporary files from accumulating. """ result = validate_multiple_record_owner([account_payee_id])[account_payee_id] if not result: return flaskify( response.Response( message={'error': error.ERROR_DONT_HAVE_PERMISSIONS}, status=401 ) ) params = get_tax_receipt_details(account_payee_id) pdf_bytes = generate_pdf(params) current_time = datetime.datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S_%f') file_name = f'{current_time}.pdf' folder_name = f'{Config.PDF_FILES_FOLDER}/account_payee_id-{account_payee_id}' s3_url = get_s3_url_for_file(pdf_bytes, file_name, folder_name) message = {'receipt_url': s3_url} return flaskify(response.Response(message=message, status=200)) @tax_details_api.route( '/account-payee//validate-session-id', methods=['GET'] ) def validate_session_id(account_payee_id): """Validate given session_id from request headers.""" result = validate_multiple_record_owner([account_payee_id])[account_payee_id] if not result: return flaskify( response.Response( message={'error': error.ERROR_DONT_HAVE_PERMISSIONS}, status=401 ) ) session_id = request.args.get('session_id') if not session_id: return flaskify( response.Response( message={'error': error.ERROR_SESSION_ID_REQUIRED}, status=400 ) ) return flaskify( response.Response( message={ 'session_id_is_valid': session_id == generate_payee_payoneer_session_id(account_payee_id), } ) ) def validate_payoneer_account_id(payoneer_id_token: str, account_payee_id: int): """Validate payoneer account.""" decoded_token = jwt.decode(payoneer_id_token, options={'verify_signature': False}) if ( not decoded_token or 'account_id' not in decoded_token or not decoded_token['account_id'] ): return False payoneer_response = get_payoneer_account(account_payee_id) if payoneer_response.status != 200: return payoneer_response payoneer_account_id = payoneer_response.message.get('result', {}).get('account_id') return ( payoneer_account_id if payoneer_account_id == decoded_token['account_id'] else False ) @tax_details_api.route( '/account-payee//tax-details-history', methods=['GET'] ) def get_payee_tax_details_history(account_payee_id): """Get payee tax details revisions.""" result = validate_multiple_record_owner([account_payee_id])[account_payee_id] if not result: return flaskify( response.Response( message={'error': error.ERROR_DONT_HAVE_PERMISSIONS}, status=401 ) ) _, history = get_tax_details(current_app.config.get('SDM_CONFIG'), account_payee_id) result = get_secure_documents_list_response(account_payee_id, history) return flaskify(response.Response(message=result)) @tax_details_api.route( '/account-payee//knr-details-history', methods=['GET'] ) def get_payee_knr_details_history(account_payee_id): """Get payee knr details revisions.""" result = validate_multiple_record_owner([account_payee_id])[account_payee_id] if not result: return flaskify( response.Response( message={'error': error.ERROR_DONT_HAVE_PERMISSIONS}, status=401 ) ) _, history = get_secure_document_details( current_app.config.get('SDM_CONFIG'), account_payee_id, KnrDetailsDocument ) result = get_secure_documents_list_response(account_payee_id, history) return flaskify(response.Response(message=result)) @tax_details_api.route('/account-payee/tax-details-dataloader', methods=['POST']) @or_( check_jwt_identity(Config.TAX_FORM_SERVICE_IDENTITIES), check_access( resource_type=PERMISSIONS_RESOURCE_TYPES.TAX_INFO, allowed_profiles=[PROFILES.DOCUMENTS], ), check_access( resource_type=PERMISSIONS_RESOURCE_TYPES.TAX_INFO, allowed_profiles=[PROFILES.ABACUS, PROFILES.ACCOUNT360], bypass=True, ), ) def dataload_tax_details_revisions(obscure_pii=True): """Dataload payee tax details revisions.""" revisions_data = request.get_json() account_payee_ids = validate_dataload_request(revisions_data) if not account_payee_ids: return flaskify( response.Response(message=error.ERROR_PAYEES_IDS_NOT_FOUND, status=404) ) result = dataload_secure_document_details( TaxDetailsDocument, dict.fromkeys(account_payee_ids, True), revisions_data, obscure_pii, ) return flaskify(result) @tax_details_api.route('/account-payee/knr-details-dataloader', methods=['POST']) def dataload_knr_details_revisions(): """Dataload payee knr details revisions.""" revisions_data = request.get_json() account_payee_ids = validate_dataload_request(revisions_data) if account_payee_ids: validated_payee_ids = validate_multiple_record_owner(account_payee_ids) return flaskify( dataload_secure_document_details( KnrDetailsDocument, validated_payee_ids, revisions_data ) ) else: return flaskify( response.Response(message=error.ERROR_PAYEES_IDS_NOT_FOUND, status=404) ) @tax_details_api.route( '/account-payee//resync-payment-readiness', methods=['POST'] ) def resync_payment_readiness_by_account_payee_id(account_payee_id): """Resync payment readiness by account_payee_id.""" message = resync_payment_readiness(account_payee_id) return flaskify(response.Response(message=message, status=201)) @tax_details_api.route( '/account-payee//tax-details/validate', methods=['GET'] ) @doc( tags=['tax details'], description='Validate tax details', ) @check_jwt_identity(Config.TAX_FORM_SERVICE_IDENTITIES) @use_kwargs(ValidateTaxDetailsParamsSchema, location='query', required=False) @marshal_with( None, code=HTTPStatus.OK, description=HTTPStatus.OK.phrase, apply=False, ) @marshal_with( None, code=HTTPStatus.NOT_FOUND, description=HTTPStatus.NOT_FOUND.phrase, apply=False, ) @marshal_with( None, code=HTTPStatus.BAD_REQUEST, description=HTTPStatus.BAD_REQUEST.phrase, apply=False, ) def validate_tax_details( account_payee_id: int, payment_entity_id: int | None = None ) -> tuple: """Endpoint to validate tax-details for a specified payee.""" tax_document = has_tax_details(account_payee_id) if not tax_document: abort( HTTPStatus.NOT_FOUND, description=error.ERROR_PAYEE_TAX_DETAILS_NOT_FOUND.format( account_payee_id=account_payee_id ), ) corp_entity = payment_entity_id or get_payee_corp_entity(account_payee_id) schema = get_tax_details_schema_for_corp_entity(corp_entity) if not schema: abort( HTTPStatus.BAD_REQUEST, description=error.ERROR_INVALID_CORP_ENTITY.format(corp_entity=corp_entity), ) if errors := schema(unknown=EXCLUDE).reverse_validate( TaxDetailsDocument.build_response(account_payee_id, tax_document) ): abort(HTTPStatus.BAD_REQUEST, description=errors) return None, HTTPStatus.OK