"""Blueprint for contract terms API.""" from abacus_common_logic.constants.error import ERROR_ENTITY_DOES_NOT_EXIST from abacus_common_logic.utils.authorization import permissions_authorize_many_accounts from abacus_common_logic.views.create_view import CreateView from abacus_common_logic.views.item_view import ItemView from abacus_common_logic.views.validations import validated_request_body from flask import Blueprint, Response, g, request, stream_with_context from owsrequest import flask_request from owsresponse import response from owsresponse.adaptors.flask import flaskify from abacus_contract.constants import error from abacus_contract.logic import ( contract as contract_logic, contract_term as logic, contract_term_transfer as transfer_logic, ) from abacus_contract.models.contract_term import ContractTerm from abacus_contract.schemas.contract_term import ( ContractTermPostSchema, ContractTermPutSchema, ContractTermsByAccountTermTypePostSchema, ContractTermSchema, ) from abacus_contract.schemas.contract_term_transfer import ( BulkAddAttachmentsRequestSchema, BulkRemoveAttachmentsRequestSchema, ) from abacus_contract.utils.authorization import pdp_authorize_many_accounts from abacus_contract.utils.dataloader import account_scoped_dataloader from abacus_contract.utils.request import get_optional_numeric_list_from_params from core.config import ows_client contract_term_api = Blueprint('contract_term_api', __name__) class ContractTermCreateView(CreateView): """Handles contract terms creation.""" post_schema = ContractTermPostSchema() def post(self, **kwargs): """Create contract term.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return super().post(**kwargs) def create_handler(self, **params): """Create contract term.""" return logic.create_contract_term(**params) class ContractTermItemView(ItemView): """Handles update, delete and get operation on existing contract_term.""" model_class = ContractTerm put_schema = ContractTermPutSchema() object_detail_schema = ContractTermSchema() def get(self, object_id): """Find object by its id or return an error.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_FORBIDDEN, message='Unauthorized', status=403, ) ) obj = self.model_class.get_by_id(object_id) if not obj or obj.deleted_at is not None or obj.deleted_by is not None: return flaskify( response.create_not_found_response( message=ERROR_ENTITY_DOES_NOT_EXIST.format( object_type='Contract Term', object_id=object_id ) ) ) body = self.object_detail_schema.dump(obj) return flaskify(response.Response(message=body, status=200)) def put(self, object_id, **kwargs): """Update a contract_term.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return super().put(object_id, **kwargs) def update_handler(self, obj, **params): """Handle contract_term updates.""" return logic.update_contract_term(obj, **params) def delete(self, object_id): """Soft delete a contract_term.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) obj = self.model_class.get_by_id_or_error(object_id) return flaskify(logic.soft_delete_contract_term_and_conditions(obj)) contract_term_api.add_url_rule( '/contract//contract-term', view_func=ContractTermCreateView.as_view('create_contract_term'), ) contract_term_api.add_url_rule( '/contract-term//', methods=['PUT', 'GET', 'DELETE'], view_func=ContractTermItemView.as_view('contract_term'), ) @contract_term_api.route('/contracts//contract-terms', methods=['GET']) def get_contract_terms_by_contract_handler(contract_id): """Handle getting contract terms associated to the specified contract.""" account_id = contract_logic.get_account_id_by_contract_id(contract_id) if not account_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_NOT_FOUND, message=error.ERROR_ACCOUNT_NOT_FOUND_FOR_CONTRACT, status=404, ) ) access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: authorized = pdp_authorize_many_accounts([account_id]) if not authorized: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403, ) ) authorized = permissions_authorize_many_accounts( ows_client, g.request_context.profile_type, g.request_context.profile_id, [account_id], ) if not authorized: return flaskify( response.create_error_response( code=error.ERROR_CODE_FORBIDDEN, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403, ) ) return flaskify(logic.get_contract_terms_by_contract(contract_id)) @contract_term_api.route('/contract-terms/dataloader', methods=['POST']) def get_contract_terms_by_contract_ids_dataloader(): """Batch-fetch contract terms for the given contract_ids.""" return account_scoped_dataloader( entity_name='Contract', resolve_accounts=contract_logic.get_account_id_map_by_contract_ids, fetch_records=logic.get_contract_term_records_by_contract_ids, key_field='contract_id', as_list=True, ) @contract_term_api.route('/contract-terms/snapshot', methods=['POST']) def get_tsv_contract_terms_snapshot(): """Get contract terms as a TSV snapshot.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) try: optional_contract_ids = get_optional_numeric_list_from_params(optional=True) except ValueError: return Response(status=400, response='contract_ids must be integers') return Response( stream_with_context(logic.contract_term_export(optional_contract_ids)), mimetype='text/tsv', ) @contract_term_api.route('/account//contract-terms/', methods=['POST']) def get_contract_terms_for_account_and_term_type(account_id): """Get contract terms for a specific account and term_type.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) post_schema = ContractTermsByAccountTermTypePostSchema() params = validated_request_body(post_schema) return flaskify( logic.get_contract_terms_for_account_and_term_type(account_id, params) ) @contract_term_api.route( '/account//contract-terms/attachments/bulk', methods=['DELETE'], ) def bulk_remove_contract_term_attachments(account_id): """Bulk remove UPCs/ISRCs from contract term attachments for a product transfer. Used by the product-transfer Step Function (PORT-12) to atomically remove transferred UPCs from product terms and transferred ISRCs from track terms on the originating account. The operation is idempotent. Request body: {"upcs": ["upc1", ...], "isrcs": ["ISRC1", ...]} Response: {"updated_terms": [...], "total_removed": int} """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) schema = BulkRemoveAttachmentsRequestSchema() params = validated_request_body(schema) result = transfer_logic.bulk_remove_from_contract_terms( account_id=account_id, upcs=params.get('upcs', []), isrcs=params.get('isrcs', []), ) return flaskify(response.Response(result, status=200)) @contract_term_api.route( '/account//contract-terms/attachments/bulk-add', methods=['POST'], ) def bulk_add_contract_term_attachments(account_id): """Bulk add UPCs/ISRCs to contract term attachments for a product transfer. Used by the product-transfer Step Function (PORT-12) to add transferred UPCs to product terms and ISRCs to track terms on the destination contract. If an active term of the required type already exists on the contract its attachments are updated; otherwise a new term and its conditions are created. Request body: {"upcs": [...], "isrcs": [...], "contract_id": int, "conditions": [{"conditions": {}, "term_rate": 80.0, "priority": 1}]} Response: {"updated_terms": [...], "created_terms": [...], "total_modified": int} """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) schema = BulkAddAttachmentsRequestSchema() params = validated_request_body(schema) result = transfer_logic.bulk_add_to_contract_terms( account_id=account_id, contract_id=params['contract_id'], upcs=params.get('upcs', []), isrcs=params.get('isrcs', []), conditions=params.get('conditions', []), attachment_relations=params.get('attachment_relations'), name=params.get('name'), ) return flaskify(response.Response(result, status=200))