"""Application Handlers. Requests are redirected to handlers, which are responsible for getting information from the URL and passing it down to the logic layer. The way each layer talks to each other is through Response objects which defines the type status of the data and the data itself. Please note: the Orchard uses the term handlers overviews as convention for clarity """ from typing import Any from flask import Response, jsonify, request from owsresponse.adaptors.flask import flaskify from owsresponse.response import Response as OwsResponse from pydantic import ValidationError from werkzeug.exceptions import HTTPException from carveouts import config from carveouts.api import app from carveouts.constants import DEFAULT_OA_USER, error, success from carveouts.logic import carveouts, vendors from carveouts.models.schemas import CarveoutPayload from carveouts.models.service_carveout import ServiceCarveout from carveouts.models.service_country_carveout import ServiceCountryPayload from carveouts.schemas.vendor import DataloadVendorContractStoreCarveoutsSchema from carveouts.utils import utils @app.route("/release_dms_carveouts/", methods=["DELETE"]) @utils.verify_grass_headers @utils.verify_grass_ownership def delete_release_dms_carveouts(product_id: int) -> Response: """Delete Release level DMS restriction. Args: product_id (int): Release/Product id for which Release DMS Master carveouts to be deleted. Returns: Response: Response containing delete success message or error response message. """ carveouts.delete_product_service_country_carveouts(product_id) return flaskify( OwsResponse( message=success.DELETE_RELEASE_LEVEL_DMS_RESTRICTION_MSG, status=200, ) ) @app.route(config.HEALTH_CHECK, methods=["GET"]) def health() -> Response: """Check the health of the application.""" return jsonify({"status": "ok"}) @app.route("/favicon.ico", methods=["GET"]) def favicon() -> Response: """Avoid favicon.ico 404 error.""" return jsonify({"status": "ok"}) @app.errorhandler(Exception) def exception_handler(exc: Exception) -> Response: """Handle error when uncaught exception is raised. Default exception handler. Note: Exception will also be sent to Sentry if config.SENTRY is set. Returns: Response: A 500 response with JSON 'code' & 'message' payload. """ app.logger.exception(exc) return flaskify(OwsResponse(message=str(exc), status=500)) @app.errorhandler(HTTPException) def http_exception_handler(exc: HTTPException) -> Response: """Handle error when HTTPException is raised. Returns: Response: A HTTP status code response with JSON 'code' & 'message' payload. """ app.logger.exception(exc) return flaskify(OwsResponse(message=str(exc), status=exc.code)) @app.route("/release_dms_master_carveouts/", methods=["DELETE"]) @utils.verify_grass_headers @utils.verify_grass_ownership def delete_release_dms_master_carveouts(product_id: int) -> Response: """Delete Release level DMS master restriction. Args: product_id (int) : Release/Product id for which Release DMS Master carveouts to be deleted. Returns: Response: Response containing delete success message or error response message. """ carveouts.delete_product_service_carveouts(product_id) return flaskify( OwsResponse( message=success.DELETE_RELEASE_LEVEL_DMS_MASTER_RESTRICTION_MSG, status=200, ) ) @app.route("/vendor-contract-store-carveouts-dataloader", methods=["POST"]) def dataload_vendor_contract_store_carveouts() -> Response: """Dataload vendor store carveouts. Returns: Response: on success, 201 status with JSON body. """ req_json = request.get_json() try: # Validate input DataloadVendorContractStoreCarveoutsSchema().load(req_json, many=True) except Exception as exc: return flaskify( OwsResponse( message=f"{error.ERROR_MESSAGE_INVALID_DATALOAD}: {str(exc)}", status=400, ) ) result = vendors.dataload_vendor_contract_store_carveouts(req_json) return flaskify(OwsResponse(message=result, status=200)) @app.route( "/delivery-restrictions/product/", methods=["GET"], ) def get_product_carveouts(product_id: int) -> Response: """Get product carveouts from vendor, subaccount and product levels. Returns: Response { “account”: { “service”: [ {} ], “country”: [...], “service_country”: [...], “new_service_opt_outs”: [...], }, “subaccount”: { “service”: [...], “country”: [...], “service_country”: [...], }, “product”: { “service”: [...], “country”: [...], “service_country”: [...], “new_service_opt_outs”: [...], } } """ return Response( carveouts.get_product_carveouts(product_id).model_dump_json(), mimetype="application/json", ) @app.route( "/delivery-restrictions/subaccount/", methods=["GET"], ) def get_subaccount_carveouts(subaccount_id: int) -> Response: """Get subaccount carveouts from vendor and subaccount levels. Returns: Response { “account”: { “service”: [ {} ], “country”: [...], “service_country”: [...], “new_service_opt_outs”: [...], }, “subaccount”: { “service”: [...], “country”: [...], “service_country”: [...], }, } """ return Response( carveouts.get_subaccount_carveouts(subaccount_id).model_dump_json(), mimetype="application/json", ) @app.route( "/delivery-restrictions/account/", methods=["GET"], ) def get_account_carveouts(account_id: int) -> Response: """Get account carveouts from vendor level. Returns: Response { “account”: { “service”: [ {} ], “country”: [...], “service_country”: [...], “new_service_opt_outs”: True, }, } """ return Response( carveouts.get_account_carveouts(account_id).model_dump_json(), mimetype="application/json", ) @app.route("/product_country_carveouts/", methods=["DELETE"]) def delete_product_country_carveouts(product_id: int) -> Response: """Delete product level country carveouts. Args: product_id (int): Release/Product id Returns: Response: Response containing delete success message. """ carveouts.delete_product_country_carveouts(product_id) return flaskify( OwsResponse( message="Product country carveouts deleted successfully.", status=204, ) ) @app.route( "/delivery-restrictions/product/", methods=["POST"], ) def save_product_carveouts(product_id: int) -> Response: """Save product carveouts from product level. Returns: Response """ request_json = request.get_json() if not request_json: return Response("No data provided.", status=400, mimetype="application/json") try: carveout_payload = _process_restriction_payload(request_json) except ValidationError as e: return Response( f"Invalid data provided: {e}", status=400, mimetype="application/json", ) carveouts.save_product_carveouts(product_id=product_id, payload=carveout_payload) return Response(status=204) @app.route( "/delivery-restrictions/account/", methods=["POST"], ) def save_account_carveouts(vendor_contract_id: int) -> Response: """Save account carveouts. Returns: Response """ request_json = request.get_json() if not request_json: return Response("No data provided.", status=400, mimetype="application/json") try: carveout_payload = _process_restriction_payload(request_json) except ValidationError as e: return Response( f"Invalid data provided: {e}", status=400, mimetype="application/json", ) carveouts.save_account_carveouts( vendor_contract_id=vendor_contract_id, payload=carveout_payload ) return Response(status=204) def _process_restriction_payload(data: dict[str, Any]) -> CarveoutPayload: return CarveoutPayload( service=( [ServiceCarveout(**item) for item in data["service"]] if data.get("service") is not None else None ), service_country=( [ServiceCountryPayload(**item) for item in data["service_country"]] if data.get("service_country") is not None else None ), country=data.get("country"), opt_out=data.get("new_service_opt_out"), updated_by=data.get("updated_by", DEFAULT_OA_USER), ) @app.route( "/delivery-restrictions/apply-default-restrictions/", methods=["POST"], ) def create_new_store_carveouts(service_id: int) -> Response: """Create new store carveouts based on opt-outs Returns: Response """ updated_by = DEFAULT_OA_USER request_json = request.get_json() if request_json: updated_by = request_json.get("updated_by", DEFAULT_OA_USER) carveouts.create_new_service_carveouts(service_id=service_id, updated_by=updated_by) return Response(status=204)