"""Arcade Starter Tools for Zoho_Books

DO NOT EDIT THIS MODULE DIRECTLY.

THIS MODULE WAS AUTO-GENERATED BY TRANSPILING THE API STARTER TOOL JSON DEFINITIONS
IN THE ../wrapper_tools DIRECTORY INTO PYTHON CODE. ANY CHANGES TO THIS MODULE WILL
BE OVERWRITTEN BY THE TRANSPILER.
"""

import asyncio
import json
from enum import Enum
from typing import Annotated, Any

import httpx
import jsonschema
from arcade_tdk import ToolContext, tool
from arcade_tdk.auth import OAuth2
from arcade_tdk.errors import RetryableToolError

from .request_body_schemas import REQUEST_BODY_SCHEMAS

# Retry configuration
INITIAL_RETRY_DELAY = 0.5  # seconds

HTTP_CLIENT = httpx.AsyncClient(
    timeout=httpx.Timeout(60.0, connect=10.0),
    limits=httpx.Limits(max_keepalive_connections=20, max_connections=100),
    transport=httpx.AsyncHTTPTransport(retries=3),
    http2=True,
    follow_redirects=True,
)


class ToolMode(str, Enum):
    """Mode for tools with complex request bodies."""

    GET_REQUEST_SCHEMA = "get_request_schema"
    EXECUTE = "execute"


def remove_none_values(data: dict[str, Any]) -> dict[str, Any]:
    return {k: v for k, v in data.items() if v is not None}


async def make_request(
    url: str,
    method: str,
    params: dict[str, Any] | None = None,
    headers: dict[str, Any] | None = None,
    content: str | None = None,
    max_retries: int = 3,
) -> httpx.Response:
    """Make an HTTP request with retry logic for 5xx server errors."""
    for attempt in range(max_retries):
        try:
            response = await HTTP_CLIENT.request(
                url=url,
                method=method,
                params=params,
                headers=headers,
                content=content,
            )
            response.raise_for_status()
        except httpx.HTTPStatusError as e:
            # Only retry on 5xx server errors
            if e.response.status_code >= 500 and attempt < max_retries - 1:
                # Exponential backoff: 0.5s, 1s, 2s
                await asyncio.sleep(INITIAL_RETRY_DELAY * (2**attempt))
                continue
            # Re-raise for 4xx errors or if max retries reached
            raise
        except httpx.RequestError:
            # Don't retry request errors (network issues are handled by transport)
            raise
        else:
            return response

    # This should never be reached, but satisfies type checker
    raise httpx.RequestError("Max retries exceeded")  # noqa: TRY003


async def make_request_with_schema_validation(
    url: str,
    method: str,
    request_data: dict[str, Any],
    schema: dict[str, Any],
    params: dict[str, Any] | None = None,
    headers: dict[str, Any] | None = None,
    max_retries: int = 3,
) -> httpx.Response:
    """Make an HTTP request with schema validation on format errors."""
    try:
        response = await make_request(
            url=url,
            method=method,
            params=params,
            headers=headers,
            content=json.dumps(request_data),
            max_retries=max_retries,
        )
    except httpx.HTTPStatusError as e:
        # Only provide schema validation for format-related errors
        if e.response.status_code in (400, 422):
            # Run validation to provide additional context
            is_valid, validation_error = validate_json_against_schema(request_data, schema)

            api_error_details = f"API returned {e.response.status_code}: {e.response.text}"

            if not is_valid:
                # Schema validation found issues - additional context
                additional_context = (
                    f"{api_error_details}\n\n"
                    f"Schema validation found the following issues:\n"
                    f"{validation_error}"
                )
            else:
                # Schema validation passed - just show API error
                additional_context = api_error_details

            raise RetryableToolError(
                message=(f"API request failed with validation error: {e.response.status_code}"),
                developer_message=api_error_details,
                additional_prompt_content=additional_context,
            ) from e
        else:
            # For non-validation errors, re-raise as-is
            raise
    else:
        return response


def validate_json_against_schema(
    json_data: dict[str, Any], schema: dict[str, Any]
) -> tuple[bool, str | None]:
    """Validate JSON data against an OpenAPI/JSON Schema.

    This provides full JSON Schema Draft 7 validation including:
    - Required fields, types, enums
    - Pattern validation (regex)
    - Format validation (email, uuid, date-time, etc.)
    - Min/max length and values
    - oneOf, anyOf, allOf
    - And all other JSON Schema features

    Args:
        json_data: The JSON data to validate
        schema: The JSON Schema to validate against

    Returns:
        Tuple of (is_valid, error_messages). If valid, error_messages is None.
        If invalid, error_messages contains all validation errors.
    """
    try:
        validator = jsonschema.Draft7Validator(
            schema, format_checker=jsonschema.Draft7Validator.FORMAT_CHECKER
        )
        # Collect ALL validation errors
        errors = list(validator.iter_errors(json_data))
        if errors:
            # Format all errors with their paths
            error_messages = []
            for error in errors:
                error_path = ".".join(str(p) for p in error.path) if error.path else "root"
                error_messages.append(f"{error.message} at {error_path}")
            # Join all errors with newlines
            return False, "\n".join(error_messages)
        else:
            return True, None
    except jsonschema.SchemaError as e:
        return False, f"Invalid schema: {e.message}"
    except Exception as e:
        return False, f"Validation error: {e!s}"


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_bank_account(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which to create the bank or credit card account.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_bank_account'."]:
    """Create a bank or credit card account in your organization.

    This tool is used to create a new bank account or credit card account for your organization via Zoho Books. Call this tool when you need to add financial accounts to manage and track transactions in your organization's accounting system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEBANKACCOUNT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEBANKACCOUNT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEBANKACCOUNT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bankaccounts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEBANKACCOUNT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_bank_accounts(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. This is required to list all associated bank and credit card accounts in Zoho Books.",  # noqa: E501
    ],
    account_status_filter: Annotated[
        str | None,
        "Specify the status to filter accounts: 'Status.All', 'Status.Active', or 'Status.Inactive'.",  # noqa: E501
    ] = None,
    sort_by: Annotated[
        str | None,
        "Specify the sorting criterion for the accounts. Options: 'account_name', 'account_type', 'account_code'.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None, "The page number of results to retrieve. Defaults to 1 if not specified."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to be fetched per page. Default value is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_bank_accounts'."]:
    """List all bank and credit card accounts for your organization.

    Use this tool to retrieve all bank and credit card accounts associated with your organization in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "filter_by": account_status_filter,
            "sort_column": sort_by,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_bank_account_zoho_books(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization to be modified. Required for identifying the specific organization's bank account.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_account_id: Annotated[
        str | None,
        "Unique identifier of the bank account to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_bank_account'."]:
    """Modify a bank account in Zoho Books.

    Use this tool to update details of an existing bank account in Zoho Books. It should be called when there's a need to modify account information such as account name or details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEBANKACCOUNTZOHOBOOKS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bank_account_id:
        missing_params.append(("bank_account_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBANKACCOUNTZOHOBOOKS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBANKACCOUNTZOHOBOOKS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bankaccounts/{account_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=bank_account_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEBANKACCOUNTZOHOBOOKS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_bank_account_details(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID for the organization. Required to specify which organization's data to access.",  # noqa: E501
    ],
    bank_account_id: Annotated[
        str, "Unique identifier used to specify the bank account for detailed retrieval."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_bank_account'."]:
    """Retrieve detailed information of a specified bank account.

    Call this tool to get a comprehensive view of a bank account by specifying its account ID. Useful for accessing detailed account data within Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/{account_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=bank_account_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_bank_account(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. Required to identify which organization's bank account is to be deleted.",  # noqa: E501
    ],
    bank_account_unique_id: Annotated[str, "Unique identifier of the bank account to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_bank_account'."]:
    """Delete a bank account from your organization.

    Use this tool to permanently delete a bank account associated with your organization in Zoho Books. This should be called when you need to remove an account to prevent future transactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/{account_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=bank_account_unique_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def deactivate_bank_account(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization in Zoho Books. This is required to identify which organization's bank account to deactivate.",  # noqa: E501
    ],
    bank_account_id: Annotated[
        str, "Unique identifier of the bank account to deactivate in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_bank_account_inactive'."]:
    """Deactivate a bank account in Zoho Books.

    Use this tool to mark a bank account as inactive in Zoho Books. This can be useful when an account is no longer in use and should be temporarily disabled."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/{account_id}/inactive".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=bank_account_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_bank_account(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization in Zoho Books. This ID is required to activate a bank account within the specified organization.",  # noqa: E501
    ],
    bank_account_id: Annotated[
        str, "Unique identifier of the bank account to be activated in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_bank_account_active'."]:
    """Activate a bank account in Zoho Books.

    This tool is used to mark a bank account as active in Zoho Books. It should be called when there is a need to change the status of a bank account to active."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/{account_id}/active".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=bank_account_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(requires_auth=OAuth2(id="arcade-zoho"), requires_secrets=["ZOHO_SERVER_URL"])
async def import_bank_statements(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization to import bank statements for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'import_bank_statements'."]:
    """Import bank or credit card feeds into your account.

    Use this tool to import bank or credit card statements directly into your account for seamless financial management. This should be called when you need to process new bank or credit card data into the system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["IMPORTBANKSTATEMENTS_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["IMPORTBANKSTATEMENTS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["IMPORTBANKSTATEMENTS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bankstatements".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["IMPORTBANKSTATEMENTS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_last_imported_bank_statement(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization used to retrieve the bank statement."
    ],
    bank_account_id: Annotated[
        str, "Unique identifier of the bank account for retrieving the last imported statement."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'get_last_imported_bank_statement'."
]:
    """Retrieve the last imported bank statement details for an account.

    Use this tool to get information about the most recently imported bank statement for a specified account. It provides insights into the account's latest financial activities."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/{account_id}/statement/lastimported".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=bank_account_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_last_imported_bank_statement(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. This is required to specify which organization's bank statement needs to be deleted.",  # noqa: E501
    ],
    bank_account_unique_identifier: Annotated[
        str, "Unique identifier for the bank account from which the statement will be deleted."
    ],
    bank_statement_id: Annotated[
        str,
        "Unique identifier of the bank statement to be deleted. Required for identifying which statement to remove.",  # noqa: E501
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'delete_last_imported_bank_statement'."
]:
    """Delete the last imported bank statement.

    Use this tool to delete the most recently imported bank statement from the specified bank account. Useful for correcting errors or removing unnecessary data."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/{account_id}/statement/{statement_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            account_id=bank_account_unique_identifier,
            statement_id=bank_statement_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def fetch_bank_account_rules(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. This ID is necessary to fetch the rules linked with the specified bank or credit card account.",  # noqa: E501
    ],
    bank_account_id: Annotated[int, "ID of the bank or credit card account to fetch rules for."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_bank_account_rules'."]:
    """Fetch rules for a specified bank account.

    This tool fetches all the rules created for a given bank or credit card account ID. It's used to retrieve details about transaction rules associated with a specific account."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/rules".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "account_id": bank_account_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_financial_account_rule(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Used to specify which organization's account rules are being altered.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_bank_account_rule'."]:
    """Create and apply rules for banking and credit accounts.

    This tool allows you to create a rule for deposits, withdrawals, refunds, or charges on bank and credit card accounts. It should be called when you need to automate financial processes by setting specific rules for account transactions.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATEFINANCIALACCOUNTRULE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEFINANCIALACCOUNTRULE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEFINANCIALACCOUNTRULE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bankaccounts/rules".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEFINANCIALACCOUNTRULE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_bank_account_rule_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which to retrieve the bank account rule details."
    ],
    bank_account_rule_id: Annotated[
        str, "Unique identifier of the bank account rule to retrieve its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_bank_account_rule'."]:
    """Retrieve details of a specific bank account rule.

    Call this tool to get information about a specific rule in a bank account using its rule ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/rules/{rule_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), rule_id=bank_account_rule_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_bank_account_rule(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization in Zoho Books for which the bank account rule needs to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_account_rule_id: Annotated[
        str | None,
        "Unique identifier for the bank account rule to update in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_bank_account_rule'."]:
    """Update or modify a bank account rule in Zoho Books.

    This tool updates an existing bank account rule in Zoho Books. It should be called when you need to make changes to a rule, either by adding new criteria or modifying existing ones. The tool provides confirmation once the rule update is successful.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEBANKACCOUNTRULE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bank_account_rule_id:
        missing_params.append(("bank_account_rule_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBANKACCOUNTRULE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBANKACCOUNTRULE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bankaccounts/rules/{rule_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), rule_id=bank_account_rule_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEBANKACCOUNTRULE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_bank_account_rule(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID representing the organization. Required for identifying the correct account.",  # noqa: E501
    ],
    bank_account_rule_id: Annotated[
        str, "Unique identifier of the bank account rule to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_bank_account_rule'."]:
    """Deletes a bank account rule, removing its effect on transactions.

    Use this tool to delete a specific bank account rule, making it inapplicable to future transactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bankaccounts/rules/{rule_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), rule_id=bank_account_rule_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_bank_transaction(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the bank transaction is to be created. This ID helps identify the specific organization within Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_bank_transaction'."]:
    """Creates a bank transaction in Zoho Books.

    This tool is used to create a bank transaction within Zoho Books. It should be called when a user needs to log a financial transaction using allowed transaction types.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATEBANKTRANSACTION_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEBANKTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEBANKTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEBANKTRANSACTION_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_bank_transactions(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "A unique ID representing the organization for which transactions are being queried. This is required to specify the context of the request.",  # noqa: E501
    ],
    bank_account_id: Annotated[
        int | None, "Unique identifier for the bank account to retrieve transactions for."
    ] = None,
    transaction_type_filter: Annotated[
        str | None,
        "Specify the type of transactions to retrieve. Expected as a string, e.g., 'expense', 'income'.",  # noqa: E501
    ] = None,
    transaction_date_range: Annotated[
        str | None,
        "Specify the start and end date for the transaction date range. Use 'date_start' for the start and 'date_end' for the end date.",  # noqa: E501
    ] = None,
    transaction_amount_range: Annotated[
        float | None,
        "Set a range of transaction amounts to filter transactions. Use two numbers: start amount, end amount.",  # noqa: E501
    ] = None,
    transaction_status_list_view: Annotated[
        str | None,
        "Filter transactions by status: all, uncategorized, manually_added, matched, excluded, categorized.",  # noqa: E501
    ] = None,
    transaction_reference_number: Annotated[
        str | None, "Search for a transaction using its reference number for more precise results."
    ] = None,
    transaction_filter_type: Annotated[
        str | None,
        "Filter transactions by type: Status.All, Status.Uncategorized, Status.Categorized, Status.ManuallyAdded, Status.Excluded, Status.Matched.",  # noqa: E501
    ] = None,
    sort_transactions_by: Annotated[
        str | None, "Specify how to sort transactions. Allowed value: 'date'."
    ] = None,
    transaction_status_filter: Annotated[
        str | None,
        "Filter transactions by status: All, uncategorized, manually_added, matched, excluded, categorized.",  # noqa: E501
    ] = None,
    search_transactions_by_text: Annotated[
        str | None, "Search transactions using contact name or transaction description."
    ] = None,
    transaction_page_number: Annotated[
        int | None,
        "Page number of transactions to fetch, with a default value of 1. Used for pagination.",
    ] = 1,
    records_per_page: Annotated[
        int | None, "Specify the number of transaction records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_bank_transactions'."]:
    """Retrieve all transaction details for a bank account.

    Use this tool to obtain detailed information about all transactions involved in a specific bank account. It is helpful for financial analysis and record-keeping."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/banktransactions".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "account_id": bank_account_id,
            "transaction_type": transaction_type_filter,
            "date": transaction_date_range,
            "amount": transaction_amount_range,
            "status": transaction_status_list_view,
            "reference_number": transaction_reference_number,
            "filter_by": transaction_filter_type,
            "sort_column": sort_transactions_by,
            "transaction_status": transaction_status_filter,
            "search_text": search_transactions_by_text,
            "page": transaction_page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_bank_transaction(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization in Zoho Books for which the bank transaction is being updated. This is required to specify the organization context for the transaction update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_transaction_identifier: Annotated[
        str | None,
        "Unique identifier for the specific bank transaction to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_bank_transaction'."]:
    """Update details of a specific bank transaction.

    This tool updates the specified bank transaction with the provided information. Call this tool to make changes to existing transaction details in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEBANKTRANSACTION_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bank_transaction_identifier:
        missing_params.append(("bank_transaction_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBANKTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBANKTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/{bank_transaction_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            bank_transaction_id=bank_transaction_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEBANKTRANSACTION_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def fetch_bank_transaction_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the bank transaction details need to be fetched."
    ],
    bank_transaction_id: Annotated[
        str, "Unique identifier for the bank transaction to fetch its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_bank_transaction'."]:
    """Fetch details of a specific bank transaction by ID.

    Use this tool to retrieve detailed information about a bank transaction by providing its transaction ID. This is useful for accessing specific transaction records in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/banktransactions/{bank_transaction_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            bank_transaction_id=bank_transaction_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_bank_transaction(
    context: ToolContext,
    organization_id: Annotated[
        str, "Specify the ID of the organization to target for transaction deletion."
    ],
    bank_transaction_id: Annotated[
        str, "Unique identifier for the bank transaction to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_bank_transaction'."]:
    """Delete a bank transaction using its ID.

    Use this tool to delete a specific bank transaction by providing the transaction ID. Ideal for managing and cleaning up bank account transactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/banktransactions/{bank_transaction_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            bank_transaction_id=bank_transaction_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def find_matching_bank_transactions(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to search transactions for."],
    transaction_id: Annotated[
        str, "Unique identifier for the bank transaction to search for matching entries."
    ],
    bank_transaction_id: Annotated[str, "Unique identifier of the bank transaction to be matched."],
    transaction_type: Annotated[
        str | None,
        "Specify the type of transaction. Allowed values: deposit, refund, transfer_fund, card_payment, sales_without_invoices, expense_refund, owner_contribution, interest_income, other_income, owner_drawings, sales_return. Note: Some types are module-specific and cannot be created under this endpoint.",  # noqa: E501
    ] = None,
    filter_date_after: Annotated[
        str | None,
        "Specify the date after which transactions should be filtered. Use YYYY-MM-DD format.",
    ] = None,
    filter_date_before: Annotated[
        str | None,
        "Specify a date in YYYY-MM-DD format. Transactions before this date will be filtered.",
    ] = None,
    minimum_transaction_amount: Annotated[
        float | None,
        "Minimum amount to filter transactions. Only transactions equal to or greater than this amount are included.",  # noqa: E501
    ] = None,
    maximum_transaction_amount: Annotated[
        float | None,
        "Maximum amount for filtering transactions. Only transactions with an amount less than or equal to this value will be included.",  # noqa: E501
    ] = None,
    transaction_contact_name: Annotated[
        str | None, "Name of the contact person involved in the transaction."
    ] = None,
    transaction_reference_number: Annotated[
        str | None, "Reference number of the transaction to filter matching records."
    ] = None,
    page_number_to_fetch: Annotated[
        int | None, "Page number to fetch. Default is 1, used for pagination."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to be fetched per page. The default value is 200."
    ] = 200,
    show_all_transactions: Annotated[
        bool | None,
        "Set to true to display all transactions without applying filters; false to filter transactions.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_matching_bank_transactions'."]:
    """Find matching uncategorized bank transactions.

    This tool retrieves a list of uncategorized bank transactions that match specified criteria. It can also handle invoices, bills, and credit notes by recording and matching new payment or refund transactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/match".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=bank_transaction_id,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "transaction_id": bank_transaction_id,
            "transaction_type": transaction_type,
            "date_after": filter_date_after,
            "date_before": filter_date_before,
            "amount_start": minimum_transaction_amount,
            "amount_end": maximum_transaction_amount,
            "contact": transaction_contact_name,
            "reference_number": transaction_reference_number,
            "show_all_transactions": show_all_transactions,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def match_bank_transaction(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique identifier for the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_transaction_id: Annotated[
        str | None,
        "Unique identifier of the bank transaction to be matched.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    account_id: Annotated[
        str | None,
        "The mandatory Account ID for listing transactions to match. This is required to specify the bank account in Zoho Books.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'match_bank_transaction'."]:
    """Match an uncategorized bank transaction with an existing one.

    Use this tool to match an uncategorized transaction with an existing transaction in Zoho Books, ensuring proper categorization and reconciliation.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["MATCHBANKTRANSACTION_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bank_transaction_id:
        missing_params.append(("bank_transaction_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["MATCHBANKTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["MATCHBANKTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/match".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=bank_transaction_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["MATCHBANKTRANSACTION_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id, "account_id": account_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def unmatch_bank_transaction(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization for which the transaction unmatching is to be performed.",  # noqa: E501
    ],
    transaction_id: Annotated[
        str, "The unique identifier of the bank transaction to be unmatched."
    ],
    account_id_for_transactions: Annotated[
        str | None,
        "The mandatory ID of the account for which transactions are to be unlisted. This is essential to specify the correct account involved in the transaction.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'unmatch_bank_transaction'."]:
    """Unmatch a previously matched bank transaction.

    Use this tool to unmatch a bank transaction that was previously categorized, reverting it to an uncategorized state."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/banktransactions/{transaction_id}/unmatch".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), transaction_id=transaction_id
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_id,
            "account_id": account_id_for_transactions,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def exclude_bank_transaction(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to which the transaction belongs."],
    transaction_id: Annotated[str, "Unique identifier of the bank transaction to be excluded."],
    account_id_for_transaction_exclusion: Annotated[
        str | None,
        "The ID of the account from which a transaction will be excluded. This is a mandatory field.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'exclude_bank_transaction'."]:
    """Exclude a transaction from a bank or credit card account.

    Use this tool to exclude a specific transaction from your bank or credit card account records. Ideal for removing unwanted or incorrect entries."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/exclude".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), transaction_id=transaction_id
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_id,
            "account_id": account_id_for_transaction_exclusion,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def restore_bank_transaction(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the Zoho Books organization to restore the transaction for."
    ],
    bank_transaction_id: Annotated[
        str, "The unique identifier for the specific bank transaction to be restored."
    ],
    account_id: Annotated[
        str | None, "Mandatory Account ID for which transactions are to be restored."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'restore_bank_transaction'."]:
    """Restores an excluded bank transaction in your account.

    Use this tool to recover a previously excluded bank transaction in your Zoho Books account, identified by the transaction ID. It should be called when you need to reinstate transactions that were mistakenly removed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/restore".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=bank_transaction_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id, "account_id": account_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def categorize_bank_transaction(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "Provide the ID of the organization to categorize the transaction.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_transaction_id: Annotated[
        str | None,
        "Unique identifier of the bank transaction to be categorized.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'categorize_bank_transaction'."]:
    """Categorize an uncategorized bank transaction.

    This tool categorizes an uncategorized bank transaction by creating a new transaction. Use it when a bank transaction needs to be classified into a specific category.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CATEGORIZEBANKTRANSACTION_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not bank_transaction_id:
        missing_params.append(("bank_transaction_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZEBANKTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZEBANKTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/categorize".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=bank_transaction_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CATEGORIZEBANKTRANSACTION_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def categorize_bank_transaction_as_expense(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books. Required to identify which organization's transaction is being categorized.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_transaction_id: Annotated[
        str | None,
        "Unique identifier for the bank transaction to be categorized as an expense.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    attachment_document: Annotated[
        str | None,
        "Document file to attach with the transaction as a string (e.g., base64 encoded or URL).  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    total_number_of_files: Annotated[
        int | None,
        "Total count of files to be attached to the transaction.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    document_identifiers: Annotated[
        int | None,
        "Comma-separated list of document IDs to be attached to the transaction.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'categorize_bank_transaction_as_expense'."
]:
    """Categorize an uncategorized bank transaction as an expense.

    Use this tool to classify an uncategorized bank transaction as an expense in Zoho Books. It should be called when a user needs to organize their financial records by assigning a transaction to the expense category.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CATEGORIZEBANKTRANSACTIONASEXPENSE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not bank_transaction_id:
        missing_params.append(("bank_transaction_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZEBANKTRANSACTIONASEXPENSE_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZEBANKTRANSACTIONASEXPENSE_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/categorize/expenses".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=bank_transaction_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CATEGORIZEBANKTRANSACTIONASEXPENSE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_identifier,
            "doc": attachment_document,
            "totalFiles": total_number_of_files,
            "document_ids": document_identifiers,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def uncategorize_bank_transaction(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books for which the transaction is to be uncategorized.",  # noqa: E501
    ],
    bank_transaction_id: Annotated[
        str, "Unique identifier of the bank transaction to uncategorize."
    ],
    account_id_for_transactions: Annotated[
        str | None, "The mandatory Account ID for which transactions are to be listed."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'uncategorize_bank_transaction'."]:
    """Revert a categorized bank transaction to uncategorized.

    Use this tool to uncategorize a previously categorized bank transaction. Call this when needing to correct or revert the categorization of a transaction in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/banktransactions/{transaction_id}/uncategorize".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=bank_transaction_id,
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_id,
            "account_id": account_id_for_transactions,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def categorize_transaction_as_vendor_payment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique string ID of the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    transaction_id: Annotated[
        str | None,
        "Unique identifier of the bank transaction to categorize as vendor payment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any],
    "Response from the API endpoint 'categorize_bank_transaction_as_vendor_payment'.",
]:
    """Categorize a bank transaction as a vendor payment.

    Use this tool to categorize an uncategorized bank transaction as a vendor payment in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CATEGORIZETRANSACTIONASVENDORPAYMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not transaction_id:
        missing_params.append(("transaction_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS[
                        "CATEGORIZETRANSACTIONASVENDORPAYMENT_REQUEST_BODY_SCHEMA"
                    ],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS[
                        "CATEGORIZETRANSACTIONASVENDORPAYMENT_REQUEST_BODY_SCHEMA"
                    ],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/categorize/vendorpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), transaction_id=transaction_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CATEGORIZETRANSACTIONASVENDORPAYMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def categorize_transaction_as_payment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization in Zoho Books for which the transaction is being categorized.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_transaction_id: Annotated[
        str | None,
        "Unique identifier of the bank transaction to be categorized as Customer Payment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any],
    "Response from the API endpoint 'categorize_bank_transaction_as_customer_payment'.",
]:
    """Categorize an uncategorized transaction as a Customer Payment.

    Use this tool to categorize an uncategorized bank transaction as a Customer Payment in Zoho Books. This should be called when you need to update the categorization status of bank transactions.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CATEGORIZETRANSACTIONASPAYMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bank_transaction_id:
        missing_params.append(("bank_transaction_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZETRANSACTIONASPAYMENT_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZETRANSACTIONASPAYMENT_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/categorize/customerpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=bank_transaction_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CATEGORIZETRANSACTIONASPAYMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def categorize_transaction_as_refund(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Must match the organization in Zoho Books to categorize transactions accurately.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    transaction_id: Annotated[
        str | None,
        "Unique identifier of the bank transaction to categorize as a refund from a credit note.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'categorize_as_credit_note_refunds'."
]:
    """Categorize a transaction as a credit note refund.

    Use this tool to categorize an uncategorized bank transaction specifically as a refund from a credit note.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CATEGORIZETRANSACTIONASREFUND_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not transaction_id:
        missing_params.append(("transaction_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZETRANSACTIONASREFUND_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZETRANSACTIONASREFUND_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/categorize/creditnoterefunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), transaction_id=transaction_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CATEGORIZETRANSACTIONASREFUND_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def categorize_refund_vendor_credit(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Specify the ID of the organization for which the transaction is being categorized as a vendor credit refund.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_transaction_id: Annotated[
        str | None,
        "Unique identifier of the bank transaction to categorize as a vendor credit refund.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'categorize_as_vendor_credit_refunds'."
]:
    """Categorize transactions as vendor credit refunds.

    Use this tool to categorize an uncategorized transaction as a refund from a vendor credit in Zoho Books. Call this tool when you need to identify and organize transactions involving vendor credit refunds.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CATEGORIZEREFUNDVENDORCREDIT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bank_transaction_id:
        missing_params.append(("bank_transaction_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZEREFUNDVENDORCREDIT_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZEREFUNDVENDORCREDIT_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{transaction_id}/categorize/vendorcreditrefunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=bank_transaction_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CATEGORIZEREFUNDVENDORCREDIT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def categorize_bank_transaction_payment_refund(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique identifier for the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_statement_line_id: Annotated[
        str | None,
        "Unique identifier for the bank statement line to be categorized.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any],
    "Response from the API endpoint 'categorize_bank_transaction_as_payment_refund'.",
]:
    """Categorize bank transactions as payment refunds.

    Use this tool to categorize uncategorized bank transactions specifically as payment refunds in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CATEGORIZEBANKTRANSACTIONPAYMENTREFUND_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bank_statement_line_id:
        missing_params.append(("bank_statement_line_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS[
                        "CATEGORIZEBANKTRANSACTIONPAYMENTREFUND_REQUEST_BODY_SCHEMA"
                    ],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS[
                        "CATEGORIZEBANKTRANSACTIONPAYMENTREFUND_REQUEST_BODY_SCHEMA"
                    ],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{statement_line_id}/categorize/paymentrefunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            statement_line_id=bank_statement_line_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CATEGORIZEBANKTRANSACTIONPAYMENTREFUND_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.banking.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def categorize_vendor_payment_refund(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization whose transactions are being categorized.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bank_statement_line_id: Annotated[
        str | None,
        "Unique identifier for the bank statement line to categorize as Vendor Payment Refund.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'categorize_as_vendor_payment_refund'."
]:
    """Categorize bank transactions as Vendor Payment Refund.

    Use this tool to categorize uncategorized bank transactions as vendor payment refunds. It should be called when organizing financial data related to refunds from vendors.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CATEGORIZEVENDORPAYMENTREFUND_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bank_statement_line_id:
        missing_params.append(("bank_statement_line_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZEVENDORPAYMENTREFUND_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CATEGORIZEVENDORPAYMENTREFUND_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/banktransactions/uncategorized/{statement_line_id}/categorize/vendorpaymentrefunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            statement_line_id=bank_statement_line_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CATEGORIZEVENDORPAYMENTREFUND_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_currency_adjustment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. Required for currency adjustments.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    account_identifiers: Annotated[
        str | None,
        "Comma-separated IDs of accounts for currency adjustments in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_base_currency_adjustment'."]:
    """Create a base currency adjustment.

    This tool creates an adjustment to the base currency based on the provided information. It is used to update and manage currency values in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATECURRENCYADJUSTMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not account_identifiers:
        missing_params.append(("account_identifiers", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECURRENCYADJUSTMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECURRENCYADJUSTMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/basecurrencyadjustment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECURRENCYADJUSTMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_identifier,
            "account_ids": account_identifiers,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_base_currency_adjustments(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to retrieve currency adjustments from."
    ],
    filter_by_date: Annotated[
        str | None,
        "Filter the base currency adjustment list by specific date ranges. Allowed values are: Date.All, Date.Today, Date.ThisWeek, Date.ThisMonth, Date.ThisQuarter, Date.ThisYear.",  # noqa: E501
    ] = None,
    sort_currency_adjustment_list_by: Annotated[
        str | None,
        "Specify the sorting criterion for the currency adjustment list. Options include: adjustment_date, exchange_rate, currency_code, debit_or_credit, or gain_or_loss.",  # noqa: E501
    ] = None,
    search_by_last_modified_time: Annotated[
        str | None, "Use a timestamp to filter adjustments by their last modified time."
    ] = None,
    fetch_page_number: Annotated[
        int | None, "The page number to fetch. Defaults to 1 if not specified."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_base_currency_adjustments'."]:
    """Fetch base currency adjustments list from Zoho Books.

    This tool retrieves a list of base currency adjustments from Zoho Books. Call this tool when you need to obtain current base currency adjustments for accounting or financial reporting purposes."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/basecurrencyadjustment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "filter_by": filter_by_date,
            "sort_column": sort_currency_adjustment_list_by,
            "last_modified_time": search_by_last_modified_time,
            "page": fetch_page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_base_currency_adjustment_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization whose currency adjustment details are being retrieved.",  # noqa: E501
    ],
    base_currency_adjustment_identifier: Annotated[
        str, "Unique identifier of the base currency adjustment to retrieve its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_base_currency_adjustment'."]:
    """Retrieve base currency adjustment details by ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/basecurrencyadjustment/{base_currency_adjustment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            base_currency_adjustment_id=base_currency_adjustment_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_currency_adjustment(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization whose currency adjustment you want to delete."
    ],
    base_currency_adjustment_id: Annotated[
        str, "Unique identifier of the base currency adjustment to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_base_currency_adjustment'."]:
    """Deletes the specified base currency adjustment.

    Use this tool to delete a specific base currency adjustment by providing its ID. Useful for managing and updating financial records."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/basecurrencyadjustment/{base_currency_adjustment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            base_currency_adjustment_id=base_currency_adjustment_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_currency_adjustment_accounts(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the currency adjustment accounts are needed."
    ],
    currency_id_for_adjustment: Annotated[
        str,
        "ID of the currency to post an adjustment for. This specifies which currency is being adjusted.",  # noqa: E501
    ],
    adjustment_date: Annotated[
        str, "Specify the date for the currency adjustment in YYYY-MM-DD format."
    ],
    exchange_rate: Annotated[
        float, "Specify the exchange rate for the currency to affect transactions."
    ],
    adjustment_notes: Annotated[
        str, "Notes for the base currency adjustment, providing additional information or context."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'list_base_currency_adjustment_accounts'."
]:
    """Retrieve accounts involved in currency adjustments.

    This tool retrieves a list of accounts that have transactions affected by a specified exchange rate. Use this to find financial accounts needing currency adjustment."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/basecurrencyadjustment/accounts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "currency_id": currency_id_for_adjustment,
            "adjustment_date": adjustment_date,
            "exchange_rate": exchange_rate,
            "notes": adjustment_notes,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_vendor_bill(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in which the bill will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    attachment_file_path: Annotated[
        str | None,
        "Path to the file to attach. Accepts GIF, PNG, JPEG, JPG, BMP, and PDF formats.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_bill'."]:
    """Create a bill received from your vendor.

    Use this tool to log and manage invoices received from vendors by creating a bill in your system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEVENDORBILL_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATEVENDORBILL_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATEVENDORBILL_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bills".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEVENDORBILL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "attachment": attachment_file_path,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_all_bills(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to specify which organization's bills to list.",  # noqa: E501
    ],
    filter_by_bill_number: Annotated[
        str | None,
        "Filter bills using the bill number. Use exact matches, prefix with 'bill_number_startswith', or substring with 'bill_number_contains'.",  # noqa: E501
    ] = None,
    filter_by_reference_number: Annotated[
        str | None,
        "Filter bills by reference number. Supports exact matches, prefix matching using `reference_number_startswith`, and substring matching using `reference_number_contains`. Useful for finding bills by external references or vendor invoice numbers.",  # noqa: E501
    ] = None,
    bill_date_filter: Annotated[
        str | None,
        "Filter bills by bill date in YYYY-MM-DD format. Use for specific dates, date ranges (date_start/date_end), or relative dates (date_before/date_after).",  # noqa: E501
    ] = None,
    filter_by_status: Annotated[
        str | None,
        "Specify the status of bills to filter by. Options include 'paid', 'open', 'overdue', 'void', or 'partially_paid'.",  # noqa: E501
    ] = None,
    filter_by_description_text: Annotated[
        str | None,
        "Filter bills using description text. Supports exact matches, prefix matching with 'description_startswith', or substring matching with 'description_contains'. Useful for finding bills by line item descriptions or vendor notes.",  # noqa: E501
    ] = None,
    filter_by_vendor_name: Annotated[
        str | None,
        "Filter bills by vendor name. Use prefix matching with 'vendor_name_startswith' or substring matching with 'vendor_name_contains'.",  # noqa: E501
    ] = None,
    filter_by_total_amount: Annotated[
        float | None,
        "Filter bills by total amount using conditions like less than, greater than, etc. Specify conditions using keys like 'total_less_than' or 'total_greater_than'.",  # noqa: E501
    ] = None,
    filter_by_vendor_id: Annotated[
        int | None,
        "Unique identifier to filter bills by a specific vendor, retrieving all related bills.",
    ] = None,
    filter_by_item_id: Annotated[
        int | None,
        "Filter bills by a specific item ID. Retrieves all bills containing a particular product or service item based on its unique identifier.",  # noqa: E501
    ] = None,
    recurring_bill_identifier: Annotated[
        int | None,
        "Filter bills by a specific recurring bill ID to retrieve all bills generated from a recurring template or schedule.",  # noqa: E501
    ] = None,
    filter_by_purchase_order_id: Annotated[
        int | None,
        "Specify the Purchase Order ID to filter bills associated with a specific order. Helps track procurement workflows.",  # noqa: E501
    ] = None,
    filter_by_last_modified_time: Annotated[
        str | None,
        "Filter bills by last modification timestamp using ISO 8601 format (YYYY-MM-DDTHH:MM:SS+/-HHMM) to find bills modified at or after a specific time.",  # noqa: E501
    ] = None,
    bill_status_filter: Annotated[
        str | None,
        "Filter bills by status. Options: Status.All, Status.PartiallyPaid, Status.Paid, Status.Overdue, Status.Void, Status.Open.",  # noqa: E501
    ] = None,
    search_text: Annotated[
        str | None,
        "Filter bills using general text across bill number, reference number, and vendor name to find matches. Useful for quick searches.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None, "Specify the page number for pagination to navigate multiple pages of bills."
    ] = None,
    bills_per_page: Annotated[
        int | None,
        "Specify the number of bills to retrieve per page. Default is 200, but adjustable for performance needs and rate limits.",  # noqa: E501
    ] = 200,
    sort_by_column: Annotated[
        str | None,
        "Specify the column to sort bills by. Available options: vendor_name, bill_number, date, due_date, total, balance, created_time.",  # noqa: E501
    ] = None,
    sorting_order: Annotated[
        str | None, "Specify the sort order for bills: 'A' for ascending, 'D' for descending."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_bills'."]:
    """Retrieve all bills with pagination support.

    This tool is used to list all bills available in the system, providing pagination information to navigate through large datasets. It should be called whenever there's a need to view or analyze billing details over multiple pages."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "bill_number": filter_by_bill_number,
            "reference_number": filter_by_reference_number,
            "date": bill_date_filter,
            "status": filter_by_status,
            "description": filter_by_description_text,
            "vendor_name": filter_by_vendor_name,
            "total": filter_by_total_amount,
            "vendor_id": filter_by_vendor_id,
            "item_id": filter_by_item_id,
            "recurring_bill_id": recurring_bill_identifier,
            "purchaseorder_id": filter_by_purchase_order_id,
            "last_modified_time": filter_by_last_modified_time,
            "filter_by": bill_status_filter,
            "search_text": search_text,
            "page": page_number,
            "per_page": bills_per_page,
            "sort_column": sort_by_column,
            "sort_order": sorting_order,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_bill_by_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the bill is to be updated or created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_identifier_key: Annotated[
        str | None,
        "Specify the API name of the custom field with unique values for identifying the bill.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_field_unique_value: Annotated[
        str | None,
        "Provide the unique value from the custom field to identify and update the specific bill.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    enable_upsert: Annotated[
        bool | None,
        "Set to true to enable upsert functionality. Creates a new bill if no existing bill matches the custom field value.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_bill_using_custom_field'."]:
    """Update or create a bill using a custom field identifier.

    Use this tool to update an existing bill or create a new one by specifying a custom field's unique value. The tool retrieves the bill based on the custom field value provided. If the bill is not found and the X-Upsert option is enabled, a new bill will be created with the provided details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEBILLBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBILLBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBILLBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bills".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEBILLBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_identifier_key,
            "X-Unique-Identifier-Value": custom_field_unique_value,
            "X-Upsert": enable_upsert,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_bill_in_zoho(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. Required for updating a bill.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bill_unique_identifier: Annotated[
        str | None,
        "The unique identifier for the bill to be updated in Zoho Books. Ensure this matches the bill you intend to modify.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    file_attachment: Annotated[
        str | None,
        "File to attach. Allowed extensions: gif, png, jpeg, jpg, bmp, pdf.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_bill'."]:
    """Updates a bill by modifying details in Zoho Books.

    This tool is used to update a bill in Zoho Books. It allows you to modify details or remove line items by excluding them from the list. Call this tool when you need to make changes to an existing bill.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEBILLINZOHO_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bill_unique_identifier:
        missing_params.append(("bill_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATEBILLINZOHO_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATEBILLINZOHO_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bills/{bill_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_unique_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEBILLINZOHO_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "attachment": file_attachment,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_bill_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization for which the bill details are being retrieved."
    ],
    bill_identifier: Annotated[
        str,
        "Unique identifier of the bill to retrieve its details. This should be provided as a string.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_bill'."]:
    """Retrieve the details of a specific bill.

    Use this tool to get comprehensive details about a specific bill by providing its ID. Useful for retrieving bill information such as amount, due date, or vendor details."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_existing_bill(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization in Zoho Books needed to delete the bill."
    ],
    bill_identifier: Annotated[str, "The unique identifier of the bill you wish to delete."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_bill'."]:
    """Deletes an existing bill if no payments are applied.

    Call this tool to delete a bill in Zoho Books if the bill has no payments applied. It confirms successful deletion of the bill."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_custom_fields_in_bill(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the bill's custom fields are being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bill_identifier: Annotated[
        str | None,
        "Unique identifier of the bill to update its custom fields.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_custom_fields_in_bill'."]:
    """Update custom fields in existing bills.

    This tool is used to update the value of custom fields in existing bills. It should be called when you need to change or add information to the custom fields of a specific bill identified by its bill ID.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECUSTOMFIELDSINBILL_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bill_identifier:
        missing_params.append(("bill_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMFIELDSINBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMFIELDSINBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bill/{bill_id}/customfields".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECUSTOMFIELDSINBILL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_bill_void(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. Used to specify which organization's bill should be marked as void.",  # noqa: E501
    ],
    bill_identifier: Annotated[str, "Unique identifier of the bill to mark as void in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_bill_void'."]:
    """Mark a bill as void in Zoho Books.

    Use this tool to update the status of a bill to void in Zoho Books when you need to cancel or invalidate a bill."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/status/void".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_bill_open(
    context: ToolContext,
    organization_id: Annotated[
        str, "Provide the ID of the organization in Zoho Books to mark the bill as open."
    ],
    bill_id: Annotated[str, "Unique identifier of the bill to mark as open in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_bill_open'."]:
    """Mark a void bill as open in Zoho Books.

    Use this tool to change the status of a void bill to open in Zoho Books. Call this tool when you need to reactivate a bill that was previously voided."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/status/open".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def submit_bill_for_approval(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization in Zoho Books for which the bill is being submitted."
    ],
    bill_identifier: Annotated[str, "Unique identifier of the bill to be submitted for approval."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'submit_bill'."]:
    """Submit a bill for approval in Zoho Books.

    Use this tool to submit a specific bill for approval in Zoho Books by providing the bill ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/submit".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def approve_bill(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "ID of the organization for which the bill needs approval."
    ],
    bill_identifier: Annotated[str, "Unique identifier of the bill to be approved in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approve_bill'."]:
    """Approve a bill in Zoho Books.

    This tool approves a specified bill in Zoho Books. It should be used when you need to change the status of a bill to approved."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/approve".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_billing_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the billing address is being updated. This identifier is necessary to access specific organizational data.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bill_unique_identifier: Annotated[
        str | None,
        "Provide the unique identifier for the bill to update its billing address.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_bill_billing_address'."]:
    """Updates the billing address for a specified bill.

    This tool is used to update the billing address associated with a specific bill. It should be called when there's a need to modify the billing address details of an existing bill.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bill_unique_identifier:
        missing_params.append(("bill_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBILLINGADDRESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBILLINGADDRESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/address/billing".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_unique_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_bill_payments_list(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization for which to retrieve bill payments."
    ],
    bill_identifier: Annotated[
        str, "Unique identifier for the specific bill to retrieve payment details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_bill_payments'."]:
    """Retrieve the list of payments made for a specific bill.

    This tool provides a list of all payments made for a specified bill using the bill ID. It should be called when you need to view detailed payment history for a particular bill."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/payments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def apply_vendor_credits_to_bill(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization where credits are being applied.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bill_identifier: Annotated[
        str | None,
        "Unique identifier for the bill to apply credits to.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'apply_credits_to_bill'."]:
    """Apply vendor credits to a bill.

    This tool applies vendor credits from excess payments to a specified bill. It is useful for managing accounts and applying multiple credits at once.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "APPLYVENDORCREDITSTOBILL_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not bill_identifier:
        missing_params.append(("bill_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["APPLYVENDORCREDITSTOBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["APPLYVENDORCREDITSTOBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/credits".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["APPLYVENDORCREDITSTOBILL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_bill_payment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books for which the bill payment is to be deleted.",  # noqa: E501
    ],
    bill_identifier: Annotated[str, "Unique identifier of the bill to be deleted."],
    bill_payment_identifier: Annotated[str, "Unique identifier of the bill payment to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_bill_payment'."]:
    """Delete a payment made to a bill.

    Use this tool to delete an existing payment associated with a bill when necessary. It helps in managing and correcting payment records in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/payments/{bill_payment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            bill_id=bill_identifier,
            bill_payment_id=bill_payment_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_bill_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization to specify which organization's bill attachment to retrieve."
    ],
    bill_identifier: Annotated[
        str, "The unique identifier of the bill to retrieve its attachment."
    ],
    get_thumbnail: Annotated[
        bool | None, "Set to true to get the thumbnail of the attachment instead of the full file."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_bill_attachment'."]:
    """Retrieve the attachment from a specific bill.

    Call this tool to get the file attached to a bill using its bill ID. Useful for accessing invoices or related documents stored in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id, "preview": get_thumbnail}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def attach_file_to_bill(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The ID of the organization in Zoho Books to which the bill belongs."
    ],
    bill_id: Annotated[
        str,
        "Unique identifier of the bill for which the file will be attached. Use this to specify the target bill in Zoho Books.",  # noqa: E501
    ],
    file_attachment: Annotated[
        str | None, "File to attach to the bill. Accepted formats: gif, png, jpeg, jpg, bmp, pdf."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_bill_attachment'."]:
    """Attach a file to a specific bill.

    Use this tool to attach a file to a specific bill in Zoho Books. It should be called when you need to upload and associate documents, such as receipts or invoices, with a bill."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_id
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "attachment": file_attachment,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_bill_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization within Zoho Books. This is required to specify which organization's bill attachment is to be deleted.",  # noqa: E501
    ],
    bill_unique_identifier: Annotated[
        str, "Unique identifier for the specific bill whose attachment is to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_bill_attachment'."]:
    """Delete the file attached to a specific bill.

    This tool deletes the file attached to a specified bill in Zoho Books. It should be called when there's a need to remove an attachment associated with a bill."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_unique_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_bill_history(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "String identifier for the organization whose bill history and comments you wish to retrieve.",  # noqa: E501
    ],
    bill_identifier: Annotated[
        str, "Unique identifier of the bill to retrieve its history and comments."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_bill_comments'."]:
    """Retrieve the complete history and comments for a bill.

    This tool retrieves the entire history and comments associated with a specified bill. It is useful for reviewing past interactions and notes on a bill."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_comment_to_bill(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique identifier of the organization in Zoho Books. Required to specify which organization's bill to comment on.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bill_identifier: Annotated[
        str | None,
        "Unique identifier for the bill to add a comment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_bill_comment'."]:
    """Add a comment to a specific bill in Zoho Books.

    Use this tool to add a comment to a bill identified by its ID in Zoho Books. Useful for internal notes or communication about a bill.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDCOMMENTTOBILL_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not bill_identifier:
        missing_params.append(("bill_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["ADDCOMMENTTOBILL_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["ADDCOMMENTTOBILL_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), bill_id=bill_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDCOMMENTTOBILL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_bill_comment(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization from which the bill comment will be deleted."
    ],
    bill_identifier: Annotated[
        str,
        "The unique identifier for the bill from which the comment will be deleted. This ID is necessary to specify the correct bill in Zoho Books.",  # noqa: E501
    ],
    comment_id: Annotated[str, "Unique identifier of the comment to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_bill_comment'."]:
    """Delete a specific comment from a bill in Zoho Books.

    Use this tool to remove a comment from a specific bill in Zoho Books when the comment is no longer needed or was made in error."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/{bill_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            bill_id=bill_identifier,
            comment_id=comment_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def convert_purchase_order_to_bill(
    context: ToolContext,
    organization_id: Annotated[
        str, "Enter the ID of the organization for which the bill will be created."
    ],
    purchase_order_ids: Annotated[
        str, "Comma-separated IDs of the purchase orders to be converted into a bill."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'convert_purchase_order_to_bill'."]:
    """Fetch bill payload from purchase orders.

    This tool retrieves the bill payload for selected purchase orders using their IDs. It should be called when you want to convert purchase orders into a bill. After obtaining the payload, use it to create a bill in the system."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/bills/editpage/frompurchaseorders".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "purchaseorder_ids": purchase_order_ids,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_chart_of_account(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the account is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_chart_of_account'."]:
    """Creates an account with a specified account type.

    This tool is used to create a new account within the chart of accounts by specifying the type of account desired. It is suitable when setting up financial frameworks or managing accounting structures.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATECHARTOFACCOUNT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECHARTOFACCOUNT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECHARTOFACCOUNT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/chartofaccounts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECHARTOFACCOUNT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_chart_of_accounts(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to retrieve chart of accounts for."],
    account_type_filter: Annotated[
        str | None,
        "Filter accounts based on type and status. Options: AccountType.All, AccountType.Active, AccountType.Inactive, AccountType.Asset, AccountType.Liability, AccountType.Equity, AccountType.Income, AccountType.Expense.",  # noqa: E501
    ] = None,
    sort_accounts_by: Annotated[
        str | None, "Specify how to sort the accounts. Options: 'account_name', 'account_type'."
    ] = None,
    last_modified_time_filter: Annotated[
        str | None,
        "Fetch accounts modified since a specific timestamp, formatted as YYYY-MM-DDTHH:MM:SSZ.",
    ] = None,
    page_number: Annotated[int | None, "Specify the page number to retrieve. Default is 1."] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to retrieve per page. Defaults to 200 if not specified."
    ] = 200,
    include_balance: Annotated[
        bool | None, "Include current account balances in the response when set to true."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_chart_of_accounts'."]:
    """Retrieve a list of all chart of accounts.

    Use this tool to obtain a complete list of chart of accounts with pagination support. Ideal for financial data management."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/chartofaccounts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "showbalance": include_balance,
            "filter_by": account_type_filter,
            "sort_column": sort_accounts_by,
            "last_modified_time": last_modified_time_filter,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_account_info(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The ID of the organization for which the account will be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    account_identifier: Annotated[
        str | None,
        "Unique identifier for the account to update in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_chart_of_account'."]:
    """Updates account information in Zoho Books.

    This tool updates the chart of account details for a specified account in Zoho Books. It should be called when account information needs modification, such as changing account names, codes, or related details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEACCOUNTINFO_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not account_identifier:
        missing_params.append(("account_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEACCOUNTINFO_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEACCOUNTINFO_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/chartofaccounts/{account_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=account_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEACCOUNTINFO_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_account_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization to which the account belongs."
    ],
    account_unique_id: Annotated[str, "Unique identifier for the account details request."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_chart_of_account'."]:
    """Retrieve detailed information for a specified account.

    This tool is used to obtain detailed information about a specific account from the chart of accounts. It should be called when you need to access account specifics such as account name, type, and other related details."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/chartofaccounts/{account_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=account_unique_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_account(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier of the organization in Zoho Books."],
    account_identifier: Annotated[
        str, "The unique identifier for the account to be deleted in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_chart_of_account'."]:
    """Delete a chart of account in Zoho Books.

    Use this tool to delete an existing chart of account. Note that accounts associated with transactions or products cannot be deleted."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/chartofaccounts/{account_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=account_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_chart_of_account(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books that needs the account to be activated.",
    ],
    account_unique_identifier: Annotated[
        str, "Unique identifier of the account to be activated in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_chart_of_account_active'."]:
    """Activate a chart of account in Zoho Books.

    Use this tool to update the status of a chart of account to active in Zoho Books. This is useful when you need to ensure an account is reactivated and available for transactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/chartofaccounts/{account_id}/active".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            account_id=account_unique_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def deactivate_chart_of_account(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "ID of the organization to deactivate the chart of account for."
    ],
    account_identifier: Annotated[str, "The unique identifier of the account to be deactivated."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_chart_of_account_inactive'."]:
    """Deactivate a specific chart of account.

    Use this tool to mark a specific chart of account as inactive in Zoho Books. This is helpful when you need to manage account statuses and ensure they reflect current business requirements."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/chartofaccounts/{account_id}/inactive".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), account_id=account_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_account_transactions(
    context: ToolContext,
    organization_id: Annotated[
        str, "Unique identifier for the organization whose account transactions are being queried."
    ],
    account_id: Annotated[str, "The unique ID of the account to retrieve transactions for."],
    transaction_date_range: Annotated[
        str | None,
        "Specify a date range for searching transactions. Use 'yyyy-mm-dd' format. Supports 'date.start', 'date.end', 'date.before', and 'date.after'.",  # noqa: E501
    ] = None,
    amount_range: Annotated[
        float | None,
        "Specify the amount range to filter account transactions. Use fields like less_than, less_equals, greater_than, and greater_equals to define the criteria.",  # noqa: E501
    ] = None,
    filter_by_account_type: Annotated[
        str | None,
        "Filter accounts based on account type and status. Options: AccountType.All, AccountType.Active, AccountType.Inactive, AccountType.Asset, AccountType.Liability, AccountType.Equity, AccountType.Income, AccountType.Expense.",  # noqa: E501
    ] = None,
    transaction_type: Annotated[
        str | None, "Filter transactions by type, such as 'invoice', 'expense', or 'refund'."
    ] = None,
    sort_by: Annotated[
        str | None,
        "Specify the column to sort transactions. Possible values: 'account_name', 'account_type'.",
    ] = None,
    page_number: Annotated[
        int | None,
        "Page number to be fetched for the transaction list. Defaults to 1 if not specified.",
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Number of records to be fetched per page. Default is 200. Specify a custom integer to override.",  # noqa: E501
    ] = 200,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'list_chart_of_account_transactions'."
]:
    """Retrieve transactions for a specified account.

    Use this tool to list all transactions associated with a given account. Ideal for tracking financial activities related to specific accounts."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/chartofaccounts/transactions".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "account_id": account_id,
            "date": transaction_date_range,
            "amount": amount_range,
            "filter_by": filter_by_account_type,
            "transaction_type": transaction_type,
            "sort_column": sort_by,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_transaction(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books for which the transaction will be deleted.",  # noqa: E501
    ],
    transaction_identifier: Annotated[
        str, "Unique identifier for the transaction to be deleted in Zoho Books."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'delete_chart_of_account_transaction'."
]:
    """Delete a specified accounting transaction.

    This tool is used to delete a specified transaction from the chart of accounts in Zoho Books. Call this tool when you need to remove a transaction permanently."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/chartofaccounts/transactions/{transaction_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            transaction_id=transaction_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_contact_person(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization where the contact person will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_contact_person'."]:
    """Create a contact person for a contact in Zoho Books.

    Use this tool to add a new contact person associated with a contact in Zoho Books. It facilitates managing additional contact person details within the platform.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATECONTACTPERSON_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECONTACTPERSON_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECONTACTPERSON_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts/contactpersons".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECONTACTPERSON_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_contact_person(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID representing the organization whose contact person is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    contact_person_identifier: Annotated[
        str | None,
        "Unique identifier for the contact person to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_contact_person'."]:
    """Update an existing contact person's details.

    Use this tool to update the details of an existing contact person in the system. This is useful when changes to contact information are needed.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATECONTACTPERSON_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not contact_person_identifier:
        missing_params.append(("contact_person_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECONTACTPERSON_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECONTACTPERSON_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts/contactpersons/{contact_person_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_person_id=contact_person_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECONTACTPERSON_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_contact_person(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization to identify which entity the contact person belongs to.",
    ],
    contact_person_id: Annotated[str, "Unique identifier for the contact person to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_contact_person'."]:
    """Delete an existing contact person from the records.

    Use this tool to remove a contact person using their unique ID from the records. It should be called when a user's contact details are no longer needed or need to be removed for any reason."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/contactpersons/{contact_person_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_person_id=contact_person_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_contact_persons(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization for which the contact persons are being retrieved."
    ],
    contact_identifier: Annotated[
        str, "Unique identifier for the contact to retrieve associated persons."
    ],
    page_number: Annotated[
        int | None, "The page number to fetch when listing contact persons. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Specifies the number of contact records to retrieve per page. The default is 200.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_contact_persons'."]:
    """Retrieve contact persons for a given contact ID.

    Use this tool to list all contact persons associated with a specific contact ID, utilizing pagination."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/contactpersons".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_identifier
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_contact_person_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. This identifies which organization the contact person belongs to.",  # noqa: E501
    ],
    contact_identifier: Annotated[str, "Unique identifier for the contact in Zoho Books."],
    contact_person_identifier: Annotated[
        str, "Unique identifier of the contact person in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_contact_person'."]:
    """Retrieve details of a specific contact person.

    Use this tool to get detailed information about a specific contact person associated with a given contact in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/contactpersons/{contact_person_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_identifier,
            contact_person_id=contact_person_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_primary_contact_person(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique identifier for the organization. This is required to specify which organization's contact is being updated.",  # noqa: E501
    ],
    contact_person_identifier: Annotated[
        str, "Unique identifier for the contact person to be marked as primary."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_contact_person_primary'."]:
    """Mark a contact person as primary for a contact.

    Use this tool to designate a specific contact person as the primary contact for a particular contact. Useful for managing contact hierarchies within an organization."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/contactpersons/{contact_person_id}/primary".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_person_id=contact_person_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_business_contact(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization for which the contact is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_contact'."]:
    """Create a new business contact with comprehensive details.

    Use this tool to create a contact in Zoho Books with information such as name, company details, addresses, and more. This contact can be utilized for various business transactions like invoices and estimates.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATEBUSINESSCONTACT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEBUSINESSCONTACT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEBUSINESSCONTACT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEBUSINESSCONTACT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_contact_list(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "Provide the ID of the organization to retrieve relevant contact data."
    ],
    filter_contact_by_type: Annotated[
        str | None, "Filter contacts by type. Accepts 'customer' or 'vendor'."
    ] = None,
    contact_name_filter: Annotated[
        str | None,
        "Filter contacts by name. Use 'startswith' or 'contains' for match type. Max-length: 100 characters.",  # noqa: E501
    ] = None,
    search_by_company_name: Annotated[
        str | None,
        "Search contacts by company name. Maximum length is 100 characters. Use variants like 'company_name_startswith' and 'company_name_contains' for different search methods.",  # noqa: E501
    ] = None,
    primary_contact_first_name: Annotated[
        str | None,
        "Search contacts by the first name of the primary contact person. Use 'first_name_startswith' or 'first_name_contains'. Max-length 100.",  # noqa: E501
    ] = None,
    search_by_last_name: Annotated[
        str | None,
        "Search contacts by last name of the primary contact person. Supports 'startswith' or 'contains' options. Max-length 100.",  # noqa: E501
    ] = None,
    address_search: Annotated[
        str | None,
        "Search contacts by address field. Use 'address_startswith' or 'address_contains'. Max-length 100.",  # noqa: E501
    ] = None,
    email_search_criteria: Annotated[
        str | None,
        "Search contacts by email of the primary contact person. Use 'startswith' or 'contains' in the string to specify the search variant. Max length is 100 characters.",  # noqa: E501
    ] = None,
    contact_phone_number: Annotated[
        str | None,
        "Search contacts by primary contact's phone number. Supports 'startswith' and 'contains' variants. Max length of 100 characters.",  # noqa: E501
    ] = None,
    contact_status_filter: Annotated[
        str | None,
        "Filter contacts by status. Options include All, Active, Inactive, Duplicate, PortalEnabled, PortalDisabled, OverDue, Unpaid, CreditLimitExceed, and Crm.",  # noqa: E501
    ] = None,
    search_contacts_text: Annotated[
        str | None, "Search contacts using contact name or notes. Maximum length is 100 characters."
    ] = None,
    sort_by_column: Annotated[
        str | None,
        "Specify the column to sort contacts by. Allowed values: contact_name, first_name, last_name, email, outstanding_receivable_amount, created_time, and last_modified_time.",  # noqa: E501
    ] = None,
    crm_contact_id: Annotated[
        str | None, "CRM Contact ID to filter specific contact details."
    ] = None,
    crm_account_id: Annotated[
        str | None,
        "Specify the CRM Account ID for the contact to retrieve specific contact details.",
    ] = None,
    crm_vendor_id: Annotated[
        str | None, "The CRM Vendor ID associated with the contact, used to filter results."
    ] = None,
    page_number_to_fetch: Annotated[
        int | None, "Specify the page number to be fetched. Defaults to 1 if not provided."
    ] = 1,
    records_per_page: Annotated[
        int | None, "The number of contact records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_contacts'."]:
    """Retrieve and filter a list of contacts from Zoho Books.

    Use this tool to obtain a detailed list of contacts with options for advanced filtering by name, company, status, and more. Ideal for managing contact information and accessing financial data like outstanding amounts and credit limits. Supports pagination for efficient data handling."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "contact_type": filter_contact_by_type,
            "contact_name": contact_name_filter,
            "company_name": search_by_company_name,
            "first_name": primary_contact_first_name,
            "last_name": search_by_last_name,
            "address": address_search,
            "email": email_search_criteria,
            "phone": contact_phone_number,
            "filter_by": contact_status_filter,
            "search_text": search_contacts_text,
            "sort_column": sort_by_column,
            "zcrm_contact_id": crm_contact_id,
            "zcrm_account_id": crm_account_id,
            "zcrm_vendor_id": crm_vendor_id,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_contact_by_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization in Zoho Books. This is required to specify which organization's records to update or create.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_field_api_name: Annotated[
        str | None,
        "The API name of the unique custom field used to identify the contact.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "The unique value of the custom field used to identify the contact. Must be a non-duplicate value.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_contact_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new contact if the unique custom field value isn't found.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_contact_using_custom_field'."
]:
    """Update a contact using a unique custom field value.

    This tool updates or creates a contact in Zoho Books based on a unique custom field value. Use it when you need to modify a contact using a non-duplicate custom field value, or create a new contact if the unique value isn't found and upsert is enabled.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECONTACTBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECONTACTBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECONTACTBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECONTACTBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": create_contact_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_business_contact(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization to which the contact belongs. This is required for identifying the organization context for the update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    contact_id: Annotated[
        str | None,
        "Unique identifier for the contact to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_contact'."]:
    """Update detailed information for an existing business contact.

    Use this tool to modify comprehensive details of an existing contact, including addresses, contact persons, and payment terms. Useful for keeping contact information up-to-date with the latest business details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEBUSINESSCONTACT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not contact_id:
        missing_params.append(("contact_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBUSINESSCONTACT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBUSINESSCONTACT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEBUSINESSCONTACT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_contact_details(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique identifier of the organization for which the contact details are being retrieved.",  # noqa: E501
    ],
    contact_id: Annotated[
        str, "Unique identifier for the contact to retrieve detailed information."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_contact'."]:
    """Retrieve comprehensive details of a specific contact.

    This tool should be called to access complete information about a contact, including basic details, addresses, payment terms, and financial data such as outstanding amounts and transaction history."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_contact(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization from which the contact will be deleted."
    ],
    contact_unique_identifier: Annotated[str, "Unique identifier of the contact to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_contact'."]:
    """Delete an existing contact from the system."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_unique_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_contact(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization to which the contact belongs."
    ],
    contact_identifier: Annotated[str, "Unique identifier of the contact to be marked as active."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_contact_active'."]:
    """Activate a contact in Zoho Books.

    Use this tool to mark a contact as active in Zoho Books. It should be called when a contact's status needs to be changed from inactive to active."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/active".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_contact_inactive(
    context: ToolContext,
    organization_identifier: Annotated[str, "The unique ID of the organization in Zoho Books."],
    contact_identifier: Annotated[
        str,
        "Unique identifier of the contact in Zoho Books. Required to specify which contact to mark as inactive.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_contact_inactive'."]:
    """Mark a Zoho Books contact as inactive.

    Use this tool to mark a contact as inactive in Zoho Books, allowing for better organization and active contact management."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/inactive".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def enable_contact_portal_access(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization within Zoho Books, required to enable portal access for a contact.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    contact_unique_id: Annotated[
        str | None,
        "Unique identifier for the specific contact whose portal access is to be enabled.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'enable_contact_portal'."]:
    """Enable portal access for a specified contact in Zoho Books.

    Use this tool to enable portal access for a contact in Zoho Books, allowing them to interact with their account through the portal.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "ENABLECONTACTPORTALACCESS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not contact_unique_id:
        missing_params.append(("contact_unique_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ENABLECONTACTPORTALACCESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ENABLECONTACTPORTALACCESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/portal/enable".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_unique_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ENABLECONTACTPORTALACCESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def enable_payment_reminder(
    context: ToolContext,
    organization_id: Annotated[
        str, "Provide the unique ID of the organization for which to enable payment reminders."
    ],
    contact_unique_identifier: Annotated[
        str, "Unique identifier of the contact for whom the payment reminder is enabled."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'enable_contact_payment_reminder'."]:
    """Enable automated payment reminders for a contact.

    This tool is used to enable automated payment reminders for a specific contact, improving timely payment handling."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/paymentreminder/enable".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_unique_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def disable_contact_payment_reminder(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books for which payment reminders will be disabled.",  # noqa: E501
    ],
    contact_unique_identifier: Annotated[
        str, "Unique identifier of the contact to disable payment reminders for."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'disable_contact_payment_reminder'."
]:
    """Disable automated payment reminders for a contact.

    This tool is used to disable automated payment reminders for a specific contact in Zoho Books. It should be called when you want to stop the system from sending payment reminders to a particular contact."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/paymentreminder/disable".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_unique_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def email_contact_statement(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Required to send the email statement to a specified contact.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    contact_identifier: Annotated[
        str | None,
        "Unique identifier of the contact to send the statement to.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    statement_start_date: Annotated[
        str | None,
        "The starting date for the statement in the format [yyyy-mm-dd]. If omitted, the current month will be used.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    statement_end_date: Annotated[
        str | None,
        "End date for the statement in the format [yyyy-mm-dd]. If not provided, the current month's statement will be sent.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    attachment_files: Annotated[
        str | None,
        "Files to be attached with the statement email, in multipart/form-data format.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_contact_statement'."]:
    """Sends an email statement to a specified contact.

    This tool triggers an email statement to be sent to a specific contact using Zoho Books. If no JSON input is provided, the email will use default content. Use this tool when you need to send financial statements to clients or contacts.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "EMAILCONTACTSTATEMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not contact_identifier:
        missing_params.append(("contact_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["EMAILCONTACTSTATEMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["EMAILCONTACTSTATEMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/statements/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["EMAILCONTACTSTATEMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "start_date": statement_start_date,
            "end_date": statement_end_date,
            "multipart_or_formdata": attachment_files,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_contact_statement_mail_content(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "ID of the organization to retrieve statement mail content for the contact."
    ],
    contact_unique_identifier: Annotated[
        str, "Unique identifier for the contact to retrieve the statement mail content."
    ],
    statement_start_date: Annotated[
        str | None,
        "Start date for the statement. Use format [yyyy-mm-dd]. Defaults to current month if not provided.",  # noqa: E501
    ] = None,
    statement_end_date: Annotated[
        str | None, "End date for the statement in the format [yyyy-mm-dd]."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_contact_statement_mail'."]:
    """Retrieve the statement email content for a contact.

    Use this tool to get the content of a statement email for a specified contact in Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/statements/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_unique_identifier,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "start_date": statement_start_date,
            "end_date": statement_end_date,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_email_to_contact(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization to which the contact belongs. It is required for sending the email.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    contact_id: Annotated[
        str | None,
        "Unique identifier for the contact to send the email to.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    send_customer_statement_with_email: Annotated[
        bool | None,
        "Indicate if a customer statement PDF should be sent with the email. Use 'true' to send, 'false' otherwise.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_contact'."]:
    """Send an email directly to a specified contact.

    Use this tool to send an email to a contact by specifying their contact ID. Ideal for communicating directly with contacts via email.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["SENDEMAILTOCONTACT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not contact_id:
        missing_params.append(("contact_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDEMAILTOCONTACT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDEMAILTOCONTACT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["SENDEMAILTOCONTACT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_identifier,
            "send_customer_statement": send_customer_statement_with_email,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_contact_activity_recent_comments(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization whose contact comments you want to retrieve.",
    ],
    contact_unique_identifier: Annotated[
        str, "Unique identifier of the contact to retrieve recent comments."
    ],
    page_number_to_fetch: Annotated[
        int | None, "Page number to be fetched. Defaults to 1 if not specified."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to be fetched per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_contact_comments'."]:
    """Retrieve recent comments for a specific contact.

    Use this tool to get a list of recent comments or activities associated with a specific contact in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_unique_identifier,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_contact_addresses(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization in Zoho Books. This ID is necessary to fetch the contact's addresses within the specified organization.",  # noqa: E501
    ],
    contact_id: Annotated[str, "Unique identifier of the contact in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_contact_address'."]:
    """Retrieve addresses for a specified contact.

    Get the shipping, billing, and additional addresses of a specific contact in Zoho Books. This tool is useful for retrieving address details when needing to display or utilize contact information in applications."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/address".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_contact_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization for which the contact address will be added. This is required to specify the target organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    contact_id: Annotated[
        str | None,
        "The unique identifier for the contact to which an address will be added.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_contact_address'."]:
    """Add an additional address to a contact in Zoho Books.

    Use this tool to append a new address to an existing contact in Zoho Books. Suitable when updating contact details with more location information.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDCONTACTADDRESS_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not contact_id:
        missing_params.append(("contact_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDCONTACTADDRESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDCONTACTADDRESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/address".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDCONTACTADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_contact_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. This is required to specify which organization's contact address needs updating.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    contact_identifier: Annotated[
        str | None,
        "Unique identifier of the contact to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    address_identifier: Annotated[
        str | None,
        "Unique identifier of the address to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_contact_address'."]:
    """Edit the additional address of a contact.

    Use this tool to update the additional address associated with a contact in Zoho Books. It requires the contact ID and address ID to specify the contact and address to be updated.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATECONTACTADDRESS_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not contact_identifier:
        missing_params.append(("contact_identifier", "path"))
    if not address_identifier:
        missing_params.append(("address_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECONTACTADDRESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECONTACTADDRESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/address/{address_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_identifier,
            address_id=address_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECONTACTADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_contact_address(
    context: ToolContext,
    organization_id: Annotated[
        str, "Unique ID of the organization in Zoho Books required for address deletion."
    ],
    contact_unique_id: Annotated[
        str, "Unique identifier for the contact whose address you want to delete."
    ],
    address_identifier: Annotated[
        str, "Unique identifier of the address to be deleted for the specified contact."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_contact_address'."]:
    """Deletes an additional address of a contact.

    This tool deletes an additional address of a specific contact in Zoho Books when given the contact and address IDs. Use it to manage contact address details."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/address/{address_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_unique_id,
            address_id=address_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_contact_refund_history(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. Required to access the specific account data.",  # noqa: E501
    ],
    contact_unique_identifier: Annotated[
        str, "Unique identifier of the contact in Zoho Books for refund history retrieval."
    ],
    page_number: Annotated[
        int | None, "The page number to fetch for the contact's refund history. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Specifies how many refund records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_contact_refunds'."]:
    """Retrieve the refund history of a specific contact.

    Use this tool to get a list of all refunds associated with a particular contact in Zoho Books. This can be useful for financial reconciliations or customer service inquiries related to refund transactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            contact_id=contact_unique_identifier,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def track_contact_for1099_reporting(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books required for 1099 reporting. This should be the ID specific to the organization tracked in the U.S.A.",  # noqa: E501
    ],
    contact_unique_id: Annotated[str, "Unique identifier of the contact for 1099 tracking."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'track_contact_1099'."]:
    """Track a contact for 1099 reporting in Zoho Books.

    Use this tool to track a contact for 1099 reporting, applicable only when the organization's country is the U.S.A. Ideal for ensuring contacts meet 1099 requirements in financial records."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/track1099".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_unique_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def stop1099_tracking_for_vendor(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization where 1099 tracking will be stopped."
    ],
    vendor_contact_id: Annotated[
        str, "Unique identifier of the vendor contact to stop 1099 tracking."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'untrack_contact_1099'."]:
    """Stop 1099 payment tracking for a vendor in the U.S.

    Use to disable 1099 reporting for vendor payments in the U.S. organization."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/untrack1099".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=vendor_contact_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_unused_retainer_payments(
    context: ToolContext,
    organization_id: Annotated[
        str, "Unique identifier for the organization to filter retainer payments."
    ],
    contact_id: Annotated[
        str,
        "The unique identifier for the contact whose unused retainer payments are being retrieved. This is required to specify which contact's data you want to access.",  # noqa: E501
    ],
    filter_by_currency_id: Annotated[
        str | None, "Currency ID to filter unused retainer payments by a specific currency."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_unused_retainer_payments'."]:
    """Retrieve unused retainer payments for a contact.

    Fetch details of unused retainer payments associated with a specific contact, providing insight into available credit balances."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/contacts/{contact_id}/receivables/unusedretainerpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), contact_id=contact_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "currency_id": filter_by_currency_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_credit_note(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the credit note is being created. Required for identifying the correct entity within Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_id: Annotated[
        str | None,
        "Invoice ID for the required invoice to associate with the credit note.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    use_custom_credit_note_number: Annotated[
        bool | None,
        "Set to true to provide your own credit note number, bypassing auto-numbering.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_credit_note'."]:
    """Create a new credit note for customer adjustments.

    Use this tool to create a credit note when issuing credits to customers for reasons such as returned items, overpayments, or necessary adjustments. It supports multi-currency, custom line items, tax calculations, and workflows.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATECREDITNOTE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATECREDITNOTE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATECREDITNOTE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECREDITNOTE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "invoice_id": invoice_id,
            "ignore_auto_number_generation": use_custom_credit_note_number,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_credit_notes(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "ID of the organization for which you want to list credit notes. Required for identification and retrieval.",  # noqa: E501
    ],
    credit_note_number: Annotated[
        str | None,
        "Filter credit notes by a specific credit note number. The number must be a unique identifier, up to 100 characters.",  # noqa: E501
    ] = None,
    filter_date: Annotated[
        str | None,
        "Filter credit notes by the date they were raised. Use yyyy-mm-dd format to search for specific credit notes.",  # noqa: E501
    ] = None,
    filter_by_status: Annotated[
        str | None,
        "Specify the status to filter credit notes. Options include: 'open', 'closed', 'void', or 'draft'.",  # noqa: E501
    ] = None,
    filter_by_total_amount: Annotated[
        float | None,
        "Filter credit notes by their total amount. Input a specific total value to retrieve matching credit notes.",  # noqa: E501
    ] = None,
    filter_by_reference_number: Annotated[
        str | None, "Filter credit notes by their reference number, limited to 100 characters."
    ] = None,
    filter_by_customer_name: Annotated[
        str | None,
        "Filter credit notes by customer name. Use to search for credit notes associated with a specific customer. Max-Length is 100 characters.",  # noqa: E501
    ] = None,
    filter_by_item_name: Annotated[
        str | None, "Search for credit notes by item name. Maximum length is 100 characters."
    ] = None,
    filter_by_customer_id: Annotated[
        str | None,
        "Search for credit notes associated with a specific customer using the customer ID. Retrieve customer IDs from the contacts API.",  # noqa: E501
    ] = None,
    filter_by_item_description: Annotated[
        str | None,
        "Filter credit notes by item description. Use 'startswith:' or 'contains:' for flexible matching. Max length of 100 characters.",  # noqa: E501
    ] = None,
    filter_by_item_id: Annotated[
        str | None,
        "Filter credit notes by item ID to find notes containing a specific item. Obtain item IDs from the items API.",  # noqa: E501
    ] = None,
    filter_by_line_item_id: Annotated[
        str | None, "Search for credit notes containing a specific line item using its ID."
    ] = None,
    filter_by_tax_id: Annotated[
        str | None,
        "Filter credit notes using a specific tax ID. Retrieve the tax ID from the taxes API.",
    ] = None,
    status_filter: Annotated[
        str | None,
        "Filter credit notes by status using predefined values: 'Status.All', 'Status.Open', 'Status.Draft', 'Status.Closed', 'Status.Void'.",  # noqa: E501
    ] = None,
    search_text: Annotated[
        str | None,
        "Search credit notes across multiple fields like credit note number, customer name, and reference number. Max-length is 100 characters.",  # noqa: E501
    ] = None,
    sort_credit_notes_by_column: Annotated[
        str | None,
        "Specify the column by which to sort the credit notes. Allowed values: 'customer_name', 'creditnote_number', 'balance', 'total', 'date', and 'created_time'.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None,
        "Page number for pagination. Specify which page of results to retrieve. Default is 1.",
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Specify the number of credit notes to be returned per page for pagination. Default is 200.",  # noqa: E501
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_credit_notes'."]:
    """Retrieve and filter a list of credit notes.

    Retrieve a paginated list of credit notes using filters and sorting based on date, status, amount, customer details, items, taxes, and custom fields."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "creditnote_number": credit_note_number,
            "date": filter_date,
            "status": filter_by_status,
            "total": filter_by_total_amount,
            "reference_number": filter_by_reference_number,
            "customer_name": filter_by_customer_name,
            "item_name": filter_by_item_name,
            "customer_id": filter_by_customer_id,
            "item_description": filter_by_item_description,
            "item_id": filter_by_item_id,
            "line_item_id": filter_by_line_item_id,
            "tax_id": filter_by_tax_id,
            "filter_by": status_filter,
            "search_text": search_text,
            "sort_column": sort_credit_notes_by_column,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_credit_note_with_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization to which the credit note belongs. This identifies the target organization for the update or creation operation.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_key: Annotated[
        str | None,
        "The API name of the unique custom field used to identify the credit note.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_field_unique_value: Annotated[
        str | None,
        "The unique value for the custom field used to identify the credit note to update or create. Ensure this matches the specific custom field's unique value constraints.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_new_credit_note_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new credit note if the unique custom field value isn't found in existing credit notes.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_credit_note_using_custom_field'."
]:
    """Update or create a credit note using a custom field.

    Use this tool to modify an existing credit note or create a new one with unique values in custom fields. If the specified unique value doesn't match any existing credit note and upsert is enabled, a new credit note will be created given the necessary details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECREDITNOTEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_key,
            "X-Unique-Identifier-Value": custom_field_unique_value,
            "X-Upsert": create_new_credit_note_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_credit_note_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Required to specify which organization's credit note to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_unique_identifier: Annotated[
        str | None,
        "Unique identifier for the credit note to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to provide your own credit note number instead of using the auto-generated one.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_credit_note'."]:
    """Update details of an existing credit note.



    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECREDITNOTEDETAILS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not credit_note_unique_identifier:
        missing_params.append(("credit_note_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_unique_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "ignore_auto_number_generation": ignore_auto_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_credit_note_details(
    context: ToolContext,
    organization_identifier: Annotated[str, "The unique ID of the organization in Zoho Books."],
    credit_note_id: Annotated[
        str,
        "The unique identifier of the credit note to retrieve details for. This ID is essential for accessing the specific credit note information.",  # noqa: E501
    ],
    response_format: Annotated[
        str | None,
        "Specify the format of the credit note details: json, pdf, or html. Default is html.",
    ] = None,
    export_with_default_print_option: Annotated[
        bool | None,
        "Specify whether to export the credit note PDF with the default print option. Use 'true' or 'false'.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_credit_note'."]:
    """Retrieve details of a specific credit note using its ID.

    Call this tool to obtain detailed information about an existing credit note by providing its unique identifier. Useful for tracking, reviewing, or managing credit notes within Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "print": export_with_default_print_option,
            "accept": response_format,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_credit_note(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization whose credit note you wish to delete."
    ],
    credit_note_id: Annotated[str, "Unique identifier of the credit note to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_credit_note'."]:
    """Delete an existing credit note using its ID.

    Use this tool to delete a specific credit note by providing its unique ID. This action is irreversible."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def email_credit_note(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "ID of the organization for which the credit note is being emailed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_id: Annotated[
        str | None,
        "Unique identifier of the credit note to be emailed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    customer_id_for_credit_note: Annotated[
        str | None,
        "Customer ID for whom the credit note is raised. Used to identify the recipient of the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    email_attachments: Annotated[
        str | None,
        "The file paths or URLs of files to attach to the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_credit_note'."]:
    """Send a credit note via email.

    Use this tool to email a credit note to the intended recipient. It's useful for sending invoice adjustments to clients.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["EMAILCREDITNOTE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not credit_note_id:
        missing_params.append(("credit_note_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["EMAILCREDITNOTE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["EMAILCREDITNOTE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["EMAILCREDITNOTE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_identifier,
            "customer_id": customer_id_for_credit_note,
            "attachments": email_attachments,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_credit_note_email_content(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "ID of the organization to retrieve the credit note email content for. This is a required field.",  # noqa: E501
    ],
    credit_note_id: Annotated[
        str, "Unique identifier of the credit note to retrieve its email content."
    ],
    specified_email_template_id: Annotated[
        str | None,
        "ID of a specific email template. If not provided, defaults to customer's or the default template.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_credit_note_email'."]:
    """Retrieve email content for a given credit note.

    Use this tool to get the email content associated with a specified credit note by its ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "email_template_id": specified_email_template_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_credit_note_void(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization in Zoho Books to mark the credit note as void."
    ],
    credit_note_identifier: Annotated[
        str, "Unique identifier of the credit note to mark as void in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_credit_note_void'."]:
    """Marks a credit note as void in Zoho Books.

    Use this tool to mark a specific credit note as void, altering its status in the Zoho Books system."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/status/void".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def convert_credit_note_to_draft(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID of the organization in Zoho Books."],
    credit_note_id: Annotated[
        str, "Unique identifier of the credit note to be converted to draft."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_credit_note_draft'."]:
    """Convert a voided credit note to a draft status.

    Use this tool to change the status of a voided credit note back to draft in Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/status/draft".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_credit_note_open(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID of the organization in Zoho Books."],
    credit_note_id: Annotated[
        str, "Unique identifier of the credit note to be converted to Open status."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_credit_note_open'."]:
    """Convert a draft credit note to open status in Zoho Books.

    Use this tool to change the status of a credit note from Draft to Open in Zoho Books. This is helpful when you need to activate a credit note that has been prepared and saved as a draft."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/status/open".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def submit_credit_note_for_approval(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "Provide the ID of the organization for which the credit note is being submitted for approval.",  # noqa: E501
    ],
    credit_note_id: Annotated[
        str, "Unique identifier of the credit note to submit for approval in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'submit_credit_note'."]:
    """Submit a credit note for approval in Zoho Books.

    Call this tool to submit a credit note for approval in Zoho Books using the credit note ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/submit".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def approve_credit_note(
    context: ToolContext,
    organization_id: Annotated[
        str, "Provide the ID of the organization for which the credit note is being approved."
    ],
    credit_note_identifier: Annotated[
        str, "A unique string identifier for the specific credit note to approve."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approve_credit_note'."]:
    """Approve a credit note for a specified ID.

    Use this tool to approve a credit note by providing the specific credit note ID. This action confirms the validity and acceptance of the credit note within the system."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/approve".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_credit_note_email_history(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization for which you want to retrieve the credit note email history.",  # noqa: E501
    ],
    credit_note_id: Annotated[
        str, "Unique identifier of the credit note to retrieve its email history."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_credit_note_email_history'."]:
    """Retrieve the email history of a specific credit note.

    Use this tool to get detailed information about the email interactions associated with a specific credit note. Useful for tracking communication history."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/emailhistory".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_credit_note_billing_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books. Required to specify which organization's data is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_identifier: Annotated[
        str | None,
        "A unique identifier for the credit note to update its billing address.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_credit_note_billing_address'."
]:
    """Update the billing address for a specific credit note.

    This tool updates the billing address associated with an existing credit note in Zoho Books. It should be called when a change in billing information for a specific credit note is required.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECREDITNOTEBILLINGADDRESS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not credit_note_identifier:
        missing_params.append(("credit_note_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/address/billing".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECREDITNOTEBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_credit_note_shipping_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "ID of the organization to which the credit note belongs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_id: Annotated[
        str | None,
        "Unique identifier of the credit note to update the shipping address for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_credit_note_shipping_address'."
]:
    """Updates the shipping address of an existing credit note.

    Use this tool to update the shipping address for a specific credit note. Call this when you need to change the recipient's address on a credit note document.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECREDITNOTESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not credit_note_id:
        missing_params.append(("credit_note_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECREDITNOTESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECREDITNOTESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/address/shipping".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECREDITNOTESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_credit_note_pdf_templates(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. Required to fetch credit note templates.",
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_credit_note_templates'."]:
    """Retrieve all credit note PDF templates from Zoho Books.

    Use this tool to obtain a list of all available credit note PDF templates from Zoho Books. It's useful when you need to browse or select a specific credit note template for use."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/templates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_credit_note_template(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "Unique identifier for the organization. Required to specify which organization's credit note template will be updated.",  # noqa: E501
    ],
    credit_note_id: Annotated[str, "Unique identifier of the credit note to be updated."],
    credit_note_template_id: Annotated[
        str, "Unique identifier of the credit note template to be updated."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_credit_note_template'."]:
    """Updates the PDF template for a specified credit note.

    Use this tool to change the PDF template associated with a specific credit note by providing the credit note ID and the template ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/templates/{template_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_id,
            template_id=credit_note_template_id,
        ),
        method="PUT",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_credit_note_invoices(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "ID of the organization for which you want to list the invoices associated with the credit note.",  # noqa: E501
    ],
    credit_note_id: Annotated[
        str, "Unique identifier of the credit note to retrieve associated invoices."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_invoices_of_credit_note'."]:
    """List invoices to which the credit note is applied.

    Use this tool to retrieve a list of invoices that are associated with a specific credit note."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/invoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def apply_credit_note_to_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization within Zoho Books to which the credit note is being applied.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_id: Annotated[
        str | None,
        "Unique identifier of the credit note to apply to invoices.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'apply_credit_note_to_invoice'."]:
    """Apply credit note to existing invoices in Zoho Books.

    This tool applies a credit note to specific existing invoices within the Zoho Books platform. It should be called when you want to manage or adjust invoice balances by using available credit notes.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "APPLYCREDITNOTETOINVOICE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not credit_note_id:
        missing_params.append(("credit_note_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["APPLYCREDITNOTETOINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["APPLYCREDITNOTETOINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/invoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["APPLYCREDITNOTETOINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_credit_note_invoice(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization to identify which organization's data is being manipulated."
    ],
    credit_note_unique_id: Annotated[
        str, "Unique identifier for the credit note to delete its associated invoice credits."
    ],
    credit_note_invoice_id: Annotated[
        str, "Unique identifier of the credit note invoice to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_invoice_of_credit_note'."]:
    """Delete the credits applied to an invoice of a credit note.

    Use this tool to remove credits applied to an invoice associated with a credit note, ensuring the invoice record reflects modified credits."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/invoices/{creditnote_invoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_unique_id,
            creditnote_invoice_id=credit_note_invoice_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_credit_note_comments(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "Provide the organization's unique ID to retrieve credit note comments."
    ],
    credit_note_id: Annotated[
        str,
        "Provide the unique identifier of the credit note to retrieve its comments and history.",
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_credit_note_comments'."]:
    """Retrieve comments and history of a credit note.

    Use this tool to obtain the history and comments associated with a specific credit note by providing the credit note's ID. This can be useful for tracking changes or feedback related to the credit transaction."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_credit_note_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The string ID of the organization to which the credit note belongs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_id: Annotated[
        str | None,
        "Unique identifier of the credit note to which the comment will be added. This is required to specify the exact credit note targeted for the comment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_credit_note_comment'."]:
    """Add a comment to an existing credit note.

    Use this tool to append a comment to a specific credit note by providing the credit note's ID and the comment text. Ideal for documenting additional information or context on credit notes.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDCREDITNOTECOMMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not credit_note_id:
        missing_params.append(("credit_note_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDCREDITNOTECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDCREDITNOTECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDCREDITNOTECOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_credit_note_comment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. Required for deleting a credit note comment.",  # noqa: E501
    ],
    credit_note_id: Annotated[
        str, "Unique identifier of the credit note to which the comment belongs."
    ],
    comment_unique_identifier: Annotated[
        str, "The unique ID of the comment to be deleted from a credit note."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_credit_note_comment'."]:
    """Delete a specific comment from a credit note.

    Use this tool to delete a comment from a credit note in Zoho Books. It requires the credit note ID and the comment ID to specify which comment to remove."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_id,
            comment_id=comment_unique_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def credit_note_refund_listing(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Use this to specify which organization's credit note refunds to list.",  # noqa: E501
    ],
    customer_identifier: Annotated[
        str | None,
        "ID of the customer for whom the credit note is raised. Provide to filter refunds by customer.",  # noqa: E501
    ] = None,
    refunds_sort_column: Annotated[
        str | None,
        "Specifies the attribute to sort the credit note refunds. Use values like 'refund_mode', 'reference_number', 'date', 'creditnote_number', 'customer_name', 'amount_bcy', or 'amount_fcy'.",  # noqa: E501
    ] = None,
    pagination_page_number: Annotated[
        int | None,
        "Page number for pagination to specify which page of results to retrieve. Default is 1.",
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Number of records to display per page in the paginated results. Default is 200.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_credit_note_refunds'."]:
    """Retrieve a paginated list of credit note refunds.

    Use this tool to get a list of all credit note refunds, including pagination information for navigating through large sets of data."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "customer_id": customer_identifier,
            "sort_column": refunds_sort_column,
            "page": pagination_page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_credit_note_refunds(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to retrieve credit note refunds for."
    ],
    credit_note_id: Annotated[str, "Unique identifier of the credit note to retrieve refunds for."],
    page_number: Annotated[
        int | None, "Specify the page number to retrieve in paginated results. Default is 1."
    ] = 1,
    results_per_page: Annotated[
        int | None, "Number of records to return per page, controlling pagination. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_credit_note_refunds1'."]:
    """Retrieve refunds for a specific credit note.

    Use this tool to list all refunds associated with a particular credit note by specifying its ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number,
            "per_page": results_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def refund_credit_note(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization in Zoho Books for which the credit note refund is being processed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_id: Annotated[
        str | None,
        "Unique identifier of the credit note to refund. This is required to specify which credit note the refund applies to.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_credit_note_refund'."]:
    """Process a credit note refund in Zoho Books.

    Use this tool to issue a refund for a specified credit note in Zoho Books. It is triggered when there's a need to reimburse the amount associated with a credit note.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["REFUNDCREDITNOTE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not credit_note_id:
        missing_params.append(("credit_note_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["REFUNDCREDITNOTE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["REFUNDCREDITNOTE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), creditnote_id=credit_note_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["REFUNDCREDITNOTE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_credit_note_refund(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization for which the credit note refund is being retrieved. Use a valid organization identifier.",  # noqa: E501
    ],
    credit_note_id: Annotated[
        str, "Unique identifier of the credit note to retrieve refund details for."
    ],
    credit_note_refund_id: Annotated[
        str, "Unique identifier of the credit note refund to retrieve specific refund details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_credit_note_refund'."]:
    """Retrieve refund details for a specific credit note.

    Use this tool to obtain information about the refund of a specific credit note by providing the credit note ID and refund ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/refunds/{creditnote_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_id,
            creditnote_refund_id=credit_note_refund_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_refund_transaction(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization. Required to identify which organization's records are being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_identifier: Annotated[
        str | None,
        "Unique identifier of the credit note to update the refund transaction.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    credit_note_refund_id: Annotated[
        str | None,
        "Provide the unique identifier of the credit note refund to update its transaction details.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_credit_note_refund'."]:
    """Update the refunded transaction details.

    Use this tool to update the details of a refunded transaction associated with a specific credit note. This is helpful when corrections or updates are needed for bookkeeping or record maintenance.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEREFUNDTRANSACTION_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not credit_note_identifier:
        missing_params.append(("credit_note_identifier", "path"))
    if not credit_note_refund_id:
        missing_params.append(("credit_note_refund_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEREFUNDTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEREFUNDTRANSACTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/refunds/{creditnote_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_identifier,
            creditnote_refund_id=credit_note_refund_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEREFUNDTRANSACTION_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.creditnotes.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_credit_note_refund(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization. Required to delete a credit note refund."
    ],
    credit_note_id: Annotated[str, "Unique identifier of the credit note to be refunded."],
    credit_note_refund_id: Annotated[str, "Unique identifier of the credit note refund to delete."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_credit_note_refund'."]:
    """Delete a specific credit note refund by ID.

    Use this tool to remove a credit note refund for a given credit note and refund ID when it's no longer needed or was made in error."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/creditnotes/{creditnote_id}/refunds/{creditnote_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            creditnote_id=credit_note_id,
            creditnote_refund_id=credit_note_refund_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_currency(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the currency is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_currency'."]:
    """Create a currency for transactions in Zoho Books.

    Use this tool to add a new currency for use in transactions within Zoho Books. This is useful when expanding the supported currencies for financial activities.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATECURRENCY_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATECURRENCY_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATECURRENCY_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/currencies".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECURRENCY_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_configured_currencies(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. Required to retrieve currency data for a specific organization.",  # noqa: E501
    ],
    exclude_base_currency_filter: Annotated[
        str | None,
        "Set to exclude the base currency from the result. Use 'Currencies.ExcludeBaseCurrency'.",
    ] = None,
    page_number: Annotated[
        int | None, "The page number of currency records to fetch. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Number of currency records to fetch per page. Defaults to 200 if not specified.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_currencies'."]:
    """Retrieve the list of configured currencies in Zoho Books.

    This tool retrieves the list of all currencies configured within Zoho Books. Use it when you need to know which currencies are set up in the system."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/currencies".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "filter_by": exclude_base_currency_filter,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_currency_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization for which the currency details are being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    currency_unique_identifier: Annotated[
        str | None,
        "Unique identifier of the currency to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_currency'."]:
    """Update the details of a currency in Zoho Books.

    Use this tool to update information for a specific currency in Zoho Books by providing the currency ID.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECURRENCYDETAILS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not currency_unique_identifier:
        missing_params.append(("currency_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECURRENCYDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECURRENCYDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/currencies/{currency_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            currency_id=currency_unique_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECURRENCYDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_currency_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the currency details are requested."
    ],
    currency_identifier: Annotated[str, "Unique identifier for the currency to fetch details for."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_currency'."]:
    """Get the details of a specific currency.

    Use this tool to obtain detailed information about a particular currency by its ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/currencies/{currency_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), currency_id=currency_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def remove_currency(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID representing the organization for which the currency deletion is requested.",
    ],
    currency_identifier: Annotated[str, "Unique identifier of the currency to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_currency'."]:
    """Remove a specific currency from the system.

    This tool deletes a currency. It should be used when a currency that is no longer needed should be removed, as long as it is not associated with any transactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/currencies/{currency_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), currency_id=currency_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_currency_exchange_rates(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to retrieve exchange rates."
    ],
    currency_identifier: Annotated[
        str, "Unique identifier for the currency to retrieve exchange rates."
    ],
    exchange_rate_from_date: Annotated[
        str | None,
        "Date to start retrieving exchange rates. Returns rates from this date or nearest previous match.",  # noqa: E501
    ] = None,
    sort_by_column: Annotated[
        str | None,
        "Sorts the exchange rates by the specified column. Only 'effective_date' is allowed.",
    ] = None,
    return_current_date_exchange_rate_only: Annotated[
        bool | None,
        "Set to true to return the exchange rate only if it's available for the current date.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_exchange_rates'."]:
    """Retrieve exchange rates for a specific currency.

    This tool fetches a list of configured exchange rates for a given currency using its ID. Call this tool to obtain exchange rate information associated with a particular currency."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/currencies/{currency_id}/exchangerates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), currency_id=currency_identifier
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "from_date": exchange_rate_from_date,
            "is_current_date": return_current_date_exchange_rate_only,
            "sort_column": sort_by_column,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_exchange_rate(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the exchange rate is being created. This must be a unique identifier within Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    currency_identifier: Annotated[
        str | None,
        "Unique identifier for the currency used to create the exchange rate in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_exchange_rate'."]:
    """Create an exchange rate for a specified currency.

    This tool should be called when you need to create a new exchange rate for a specific currency in Zoho Books. It allows you to define exchange rates between specified currencies.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEEXCHANGERATE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not currency_identifier:
        missing_params.append(("currency_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEEXCHANGERATE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEEXCHANGERATE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/currencies/{currency_id}/exchangerates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), currency_id=currency_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEEXCHANGERATE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_currency_exchange_rate(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization for which you want to retrieve exchange rate details.",
    ],
    currency_unique_identifier: Annotated[
        str,
        "Unique identifier for the currency. Use to specify the currency for the exchange rate details.",  # noqa: E501
    ],
    exchange_rate_unique_id: Annotated[
        str, "Unique identifier of the exchange rate to retrieve details for."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_exchange_rate'."]:
    """Retrieve details of a specific currency exchange rate.

    This tool is used to get details of an exchange rate associated with a specified currency. It should be called when you need information about a specific exchange rate for financial or currency conversion purposes."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/currencies/{currency_id}/exchangerates/{exchange_rate_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            currency_id=currency_unique_identifier,
            exchange_rate_id=exchange_rate_unique_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_exchange_rate(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization for which the exchange rate is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    currency_unique_identifier: Annotated[
        str | None,
        "Unique identifier for the currency you want to update the exchange rate for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    exchange_rate_identifier: Annotated[
        str | None,
        "Unique identifier for the exchange rate to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_exchange_rate'."]:
    """Update exchange rate details for a currency in Zoho Books.

    This tool updates the exchange rate for a specified currency in Zoho Books. It should be called when you need to modify the exchange rate details, usually for financial reports, accounting adjustments, or compliance with real-time rates.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEEXCHANGERATE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not currency_unique_identifier:
        missing_params.append(("currency_unique_identifier", "path"))
    if not exchange_rate_identifier:
        missing_params.append(("exchange_rate_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEEXCHANGERATE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEEXCHANGERATE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/currencies/{currency_id}/exchangerates/{exchange_rate_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            currency_id=currency_unique_identifier,
            exchange_rate_id=exchange_rate_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEEXCHANGERATE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_exchange_rate(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the exchange rate is being deleted."
    ],
    currency_identifier: Annotated[
        str, "Unique identifier for the currency whose exchange rate is to be deleted."
    ],
    exchange_rate_identifier: Annotated[
        str, "Unique identifier for the exchange rate to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_exchange_rate'."]:
    """Delete an exchange rate for a specific currency.

    Use this tool to delete an exchange rate using the specified currency and exchange rate IDs."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/currencies/{currency_id}/exchangerates/{exchange_rate_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            currency_id=currency_identifier,
            exchange_rate_id=exchange_rate_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.custommodules.ALL"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_custom_module_records(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the Zoho organization to fetch records from."
    ],
    custom_module_name: Annotated[
        str, "Name of the custom module from which to retrieve records in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_records_of_custom_module'."]:
    """Fetches records from a specified custom module.

    Use this tool to retrieve the list of records from a specific custom module in Zoho Books. Provide the module name to get the corresponding records."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/{module_name}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), module_name=custom_module_name
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.custommodules.ALL"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_custom_module_records(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization that owns the module records to be updated. This ID is required to specify which organization's records are being modified.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    module_name: Annotated[
        str | None,
        "Specify the name of the custom module to update records in bulk.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'bulk_update_custom_module_records'."
]:
    """Updates existing custom module records in bulk.

    Use this tool to update multiple records in a custom module efficiently. This is useful for making uniform changes across a set of records within a specified module.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECUSTOMMODULERECORDS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not module_name:
        missing_params.append(("module_name", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMMODULERECORDS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMMODULERECORDS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/{module_name}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), module_name=module_name
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECUSTOMMODULERECORDS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.custommodules.ALL"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_custom_module(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the custom module is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_module_name: Annotated[
        str | None,
        "Specify the name for the custom module to be created in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_custom_module'."]:
    """Creates a custom module in Zoho Books.

    Use this tool to create a custom module in Zoho Books. Specify the module name as needed.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATECUSTOMMODULE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not custom_module_name:
        missing_params.append(("custom_module_name", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMMODULE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMMODULE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/{module_name}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), module_name=custom_module_name
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECUSTOMMODULE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.custommodules.ALL"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_custom_module(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    module_name: Annotated[str, "The name of the custom module to be deleted in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_custom_module'."]:
    """Deletes a specified custom module in Zoho Books.

    This tool is used to delete a custom module within Zoho Books. It should be called when you need to remove a specific custom module by its name."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/{module_name}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), module_name=module_name
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.custommodules.ALL"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_custom_module_record_details(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    module_name: Annotated[
        str, "The name of the module associated with the organization in Zoho Books."
    ],
    custom_module_id: Annotated[
        int,
        "The ID for the specific custom module in Zoho Books that you want to retrieve details for. This value should be an integer.",  # noqa: E501
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'get_custom_module_record_details'."
]:
    """Fetch details of an organization in Zoho Books.

    Use this tool to retrieve details of a specific organization by providing the module name and module ID in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/{module_name}/{module_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            module_name=module_name,
            module_id=custom_module_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.custommodules.ALL"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_custom_module_record(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    module_name: Annotated[
        str | None,
        "The name of the custom module to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_module_id: Annotated[
        int | None,
        "The ID of the custom module to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_custom_module_record'."]:
    """Update an existing custom module in Zoho Books.

    Use this tool to update an existing custom module record in Zoho Books by specifying the module name and module ID.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECUSTOMMODULERECORD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not module_name:
        missing_params.append(("module_name", "path"))
    if not custom_module_id:
        missing_params.append(("custom_module_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMMODULERECORD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMMODULERECORD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/{module_name}/{module_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            module_name=module_name,
            module_id=custom_module_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECUSTOMMODULERECORD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.custommodules.ALL"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_custom_module_record(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to specify which organization's module record to delete.",  # noqa: E501
    ],
    module_name: Annotated[str, "Name of the custom module containing the record to delete."],
    custom_module_id: Annotated[int, "The unique integer ID of the custom module to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_custom_module_record'."]:
    """Delete an individual record from a custom module.

    Use this tool to delete specific records from a custom module in Zoho Books. Provide the module name and module ID to identify and remove the desired record."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/{module_name}/{module_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            module_name=module_name,
            module_id=custom_module_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_customer_debit_note(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "A unique identifier for the organization to which the debit note will be associated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    send_debit_note: Annotated[
        bool | None,
        "Set to true to send the debit note to the associated contacts. Accepts true or false.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to ignore automatic debit note number generation, requiring manual entry.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_customer_debit_note'."]:
    """Create a customer debit note for invoice adjustments.

    This tool is used to create a customer debit note when there are additional charges or adjustments needed for an existing invoice in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATECUSTOMERDEBITNOTE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMERDEBITNOTE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMERDEBITNOTE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/customerdebitnotes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECUSTOMERDEBITNOTE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "send": send_debit_note,
            "ignore_auto_number_generation": ignore_auto_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_customer_debit_notes(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID of the organization for this request. Required for identifying the organization whose debit notes are being queried.",  # noqa: E501
    ],
    search_item_name: Annotated[
        str | None,
        "Search debit notes based on product or service names. Supports 'item_name_startswith' and 'item_name_contains'. Max length is 100 characters.",  # noqa: E501
    ] = None,
    search_by_item_id: Annotated[
        str | None,
        "Search for customer debit notes using a specific item ID to filter based on product or service identifiers.",  # noqa: E501
    ] = None,
    item_description_filter: Annotated[
        str | None,
        "Filter debit notes by item description using detailed descriptions of products or services. Supports 'startswith' and 'contains' variants. Max length: 100 characters.",  # noqa: E501
    ] = None,
    search_by_customer_name: Annotated[
        str | None,
        "Search debit notes by customer name. Filters based on the business or individual name. Maximum 100 characters.",  # noqa: E501
    ] = None,
    customer_email_filter: Annotated[
        str | None,
        "Filter debit notes by customer email address, with a maximum length of 100 characters, to find specific customers or generate segment reports.",  # noqa: E501
    ] = None,
    search_by_total_amount: Annotated[
        str | None,
        "Filter debit notes by the total amount, including taxes, discounts, and adjustments. Useful for finding specific price ranges or high-value transactions.",  # noqa: E501
    ] = None,
    search_by_outstanding_balance: Annotated[
        str | None,
        "Filter debit notes by the remaining unpaid amount owed by the customer. Useful for finding overdue debit notes, tracking receivables, or generating aging reports.",  # noqa: E501
    ] = None,
    search_by_custom_field: Annotated[
        str | None,
        "Filter debit notes using custom fields. Supports 'custom_field_startswith' and 'custom_field_contains' for searching specific text patterns.",  # noqa: E501
    ] = None,
    search_date_range: Annotated[
        str | None,
        "Filter debit notes by creation date using yyyy-mm-dd format. Supports variants: date_start, date_end, date_before, date_after.",  # noqa: E501
    ] = None,
    filter_due_date: Annotated[
        str | None,
        "Search debit notes by due date using yyyy-mm-dd format. Supports 'due_date_start', 'due_date_end', 'due_date_before', and 'due_date_after' variants.",  # noqa: E501
    ] = None,
    creation_date_filter: Annotated[
        str | None,
        "Filter debit notes by creation date. Use formats: 'yyyy-mm-dd', 'created_date_start', 'created_date_end', 'created_date_before', or 'created_date_after'.",  # noqa: E501
    ] = None,
    last_modified_timestamp: Annotated[
        str | None,
        "Filter debit notes modified after this timestamp in YYYY-MM-DDTHH:MM:SS-UTC format.",
    ] = None,
    status_filter: Annotated[
        str | None,
        "Filter debit notes by their status. Allowed values: sent, draft, overdue, paid, void, unpaid, partially_paid, viewed.",  # noqa: E501
    ] = None,
    search_by_customer_id: Annotated[
        str | None,
        "Search debit notes by the customer's unique identifier. Use the customer ID from the Contacts API to find all corresponding debit notes.",  # noqa: E501
    ] = None,
    filter_by_debit_note_type: Annotated[
        str | None,
        "Set to 'Type.DebitNote' to filter debit notes specifically. Required for this search.",
    ] = None,
    general_search_text: Annotated[
        str | None,
        "Search debit notes by number, purchase order, or customer name. Max 100 characters. Useful for quick searches across multiple fields.",  # noqa: E501
    ] = None,
    sort_debit_notes_by_column: Annotated[
        str | None,
        "Sort debit notes by a specific column. Allowed values: customer_name, debit_note_number, date, due_date, total, balance, created_time.",  # noqa: E501
    ] = None,
    page_number_to_fetch: Annotated[
        int | None,
        "Page number to retrieve from paginated results. Default is 1. Use with `per_page` to navigate extensive debit note data efficiently.",  # noqa: E501
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Specify the number of records to retrieve per page, up to a maximum of 200. The default value is 200. This helps manage data transfer efficiency.",  # noqa: E501
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_customer_debit_notes'."]:
    """Retrieve and organize customer debit notes easily.

    Use this tool to access customer debit notes with options for pagination, filtering, searching, and sorting, allowing you to view and organize debit note data efficiently."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/customerdebitnotes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "item_name": search_item_name,
            "item_id": search_by_item_id,
            "item_description": item_description_filter,
            "customer_name": search_by_customer_name,
            "email": customer_email_filter,
            "total": search_by_total_amount,
            "balance": search_by_outstanding_balance,
            "custom_field": search_by_custom_field,
            "date": search_date_range,
            "due_date": filter_due_date,
            "created_date": creation_date_filter,
            "last_modified_time": last_modified_timestamp,
            "status": status_filter,
            "customer_id": search_by_customer_id,
            "filter_by": filter_by_debit_note_type,
            "search_text": general_search_text,
            "sort_column": sort_debit_notes_by_column,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_customer_debit_note(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Organization ID for the request within Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    debit_note_unique_identifier: Annotated[
        str | None,
        "Unique identifier for the debit note to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to ignore automatic debit note number generation, requiring manual input of the debit note number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_customer_debit_note'."]:
    """Update an existing customer debit note.

    Use this tool to update details of an existing customer debit note in Zoho Books. Remove a line item by omitting it from the line_items list.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECUSTOMERDEBITNOTE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not debit_note_unique_identifier:
        missing_params.append(("debit_note_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMERDEBITNOTE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMERDEBITNOTE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{debit_note_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            debit_note_id=debit_note_unique_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECUSTOMERDEBITNOTE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "ignore_auto_number_generation": ignore_auto_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_customer_debit_note(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "Organization ID for the request. This ID is required to specify the organization from which to retrieve the debit note.",  # noqa: E501
    ],
    debit_note_unique_id: Annotated[
        str, "Unique identifier for the specific debit note to retrieve details."
    ],
    response_format: Annotated[
        str | None,
        "Format of the debit note details. Options are json, pdf, or html. Default is json.",
    ] = None,
    print_pdf: Annotated[
        bool | None,
        "If true, print the exported PDF version of the debit note; otherwise, do not print.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_customer_debit_note'."]:
    """Retrieve the details of a customer debit note from Zoho Books.

    This tool is used to get detailed information about a specific customer debit note by its ID from Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{debit_note_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            debit_note_id=debit_note_unique_id,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "print": print_pdf,
            "accept": response_format,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_customer_debit_note(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization within Zoho Books. Required to specify which organization's data to access or modify.",  # noqa: E501
    ],
    debit_note_unique_id: Annotated[str, "Unique identifier for the debit note to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_customer_debit_note'."]:
    """Delete an existing customer debit note in Zoho Books.

    Use this tool to delete a customer debit note in Zoho Books. Note that debit notes with payments or credit notes applied cannot be deleted."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{debit_note_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            debit_note_id=debit_note_unique_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_customer_payment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. This is necessary to associate the payment with the correct organization.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_customer_payment'."]:
    """Create a new customer payment in Zoho Books.

    Use this tool to record a new payment for a customer in Zoho Books. It should be called when you need to add a payment entry for accounting purposes or to update a customer's payment status.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATECUSTOMERPAYMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMERPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMERPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/customerpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECUSTOMERPAYMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_customer_payments(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization. Required to specify which organization's payments to list.",  # noqa: E501
    ],
    search_customer_name: Annotated[
        str | None,
        "Filter payments by customer name using 'startswith' or 'contains' variants. Max length: 100 characters.",  # noqa: E501
    ] = None,
    search_by_reference_number: Annotated[
        str | None,
        "Search payments by reference number. Supports 'startswith' and 'contains' variants. Max-length 100.",  # noqa: E501
    ] = None,
    payment_date: Annotated[
        str | None,
        "Specify the date of the customer payment in YYYY-MM-DD format to filter results.",
    ] = None,
    payment_amount_filter: Annotated[
        float | None,
        "Filter payments by amount using variants: less_than, less_equals, greater_than, or greater_equals.",  # noqa: E501
    ] = None,
    search_by_customer_notes: Annotated[
        str | None,
        "Search payments using customer notes, supporting 'startswith' and 'contains' variants.",
    ] = None,
    payment_mode_filter: Annotated[
        str | None,
        "Filter payments by specifying the payment mode. Use 'startswith' or 'contains' for partial matching.",  # noqa: E501
    ] = None,
    filter_payments_by_mode: Annotated[
        str | None,
        "Filter payments by the payment mode. Accepted values include: All, Check, Cash, BankTransfer, Paypal, CreditCard, GoogleCheckout, Credit, Authorizenet, BankRemittance, Payflowpro, Stripe, TwoCheckout, Braintree, Others.",  # noqa: E501
    ] = None,
    sort_column: Annotated[
        str | None,
        "Specify the column to sort the payments by. Common options include date, amount, or customer name.",  # noqa: E501
    ] = None,
    search_term_for_payments: Annotated[
        str | None,
        "Search payments by reference number, customer name, or payment description. Maximum length is 100 characters.",  # noqa: E501
    ] = None,
    customer_id: Annotated[
        str | None,
        "The unique identifier for the customer involved in the payment. Use this to target specific customer transactions.",  # noqa: E501
    ] = None,
    page_number_to_fetch: Annotated[
        int | None, "The page number of payment records to be retrieved. Defaults to 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to be fetched per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_customer_payments'."]:
    """List all payments made by your customers.

    Use this tool to retrieve a list of all the payments made by your customers. It is useful for financial analysis, reconciliation, or customer support purposes."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/customerpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "customer_name": search_customer_name,
            "reference_number": search_by_reference_number,
            "date": payment_date,
            "amount": payment_amount_filter,
            "notes": search_by_customer_notes,
            "payment_mode": payment_mode_filter,
            "filter_by": filter_payments_by_mode,
            "sort_column": sort_column,
            "search_text": search_term_for_payments,
            "customer_id": customer_id,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def bulk_delete_customer_payments(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier string for the organization in Zoho Books."
    ],
    payment_ids_to_delete: Annotated[
        str, "Comma-separated list of payment IDs to be deleted in the bulk operation."
    ],
    perform_bulk_delete: Annotated[
        bool, "Set to true to perform the bulk delete operation for customer payments."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_delete_customer_payments'."]:
    """Delete multiple customer payments efficiently.

    Use this tool to remove several customer payments at once in Zoho Books. It's ideal for managing large-scale payment data cleanup."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/customerpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="DELETE",
        params=remove_none_values({
            "organization_id": organization_id,
            "payment_id": payment_ids_to_delete,
            "bulk_delete": perform_bulk_delete,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_payment_by_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization whose payment is being updated or created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "API name of the unique custom field used to identify the payment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "The unique value of the custom field used to identify or create a payment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_new_payment_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new payment when no matching unique custom field value is found.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_customer_payment_using_custom_field'."
]:
    """Update or upsert a customer payment using a unique custom field.

    Use this tool to update an existing customer payment or create a new one if it doesn't exist, by specifying a unique value from a custom field. The unique custom field value helps identify the payment to update. If upsert is enabled, a new payment will be created when the unique value is not found.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEPAYMENTBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPAYMENTBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPAYMENTBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/customerpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPAYMENTBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": create_new_payment_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_customer_payment_info(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the payment update is requested.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    payment_unique_identifier: Annotated[
        str | None,
        "The unique identifier for the payment to be updated. Use this to specify which payment you want to modify.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_customer_payment'."]:
    """Update an existing payment information.

    Use this tool to update payment information for a customer when any corrections or modifications are needed.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECUSTOMERPAYMENTINFO_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not payment_unique_identifier:
        missing_params.append(("payment_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMERPAYMENTINFO_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMERPAYMENTINFO_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/customerpayments/{payment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            payment_id=payment_unique_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECUSTOMERPAYMENTINFO_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_customer_payment_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the payment details are requested."
    ],
    payment_identifier: Annotated[
        str, "The unique identifier of the payment to retrieve details for."
    ],
    response_format: Annotated[
        str | None, "Format of the response. Allowed values: 'json' or 'pdf'. Default is 'json'."
    ] = "json",
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_customer_payment'."]:
    """Retrieve details of a specific customer payment.

    Use this tool to obtain information about a specific customer payment by providing the payment ID. Ideal for retrieving payment history or verifying payment details."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/customerpayments/{payment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=payment_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id, "accept": response_format}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_customer_payment(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization whose payment is being deleted."
    ],
    payment_identifier: Annotated[
        str,
        "Unique identifier for the payment to be deleted. Required to specify which payment record should be removed.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_customer_payment'."]:
    """Delete an existing payment for a customer.

    Use this tool when you need to delete a specific customer payment by its payment ID. It confirms the removal of the payment record."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/customerpayments/{payment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=payment_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_customer_payment_refunds(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization. Required to specify which organization's data to access.",  # noqa: E501
    ],
    customer_payment_identifier: Annotated[
        str, "Unique identifier of the customer payment to retrieve associated refunds."
    ],
    page_number: Annotated[
        int | None, "Specify the page number to fetch. Defaults to 1 if not provided."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Defaults to 200 if not specified."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_customer_payment_refunds'."]:
    """Retrieve refunds for a specified customer payment.

    Use this tool to list all refunds related to a particular customer payment by providing the customer payment ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/customerpayments/{customer_payment_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            customer_payment_id=customer_payment_identifier,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def refund_excess_payment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization to process the refund under. Ensure this matches the ID in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    customer_payment_identifier: Annotated[
        str | None,
        "Unique identifier for the customer's payment to be refunded.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_customer_payment_refund'."]:
    """Refund the excess amount paid by a customer.

    Use this tool to refund any excess amount received from a customer payment. It is called when there is a need to process a refund for overpaid amounts in customer transactions.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["REFUNDEXCESSPAYMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not customer_payment_identifier:
        missing_params.append(("customer_payment_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["REFUNDEXCESSPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["REFUNDEXCESSPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/customerpayments/{customer_payment_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            customer_payment_id=customer_payment_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["REFUNDEXCESSPAYMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_customer_payment_custom_fields(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization to which the customer payment belongs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    customer_payment_identifier: Annotated[
        str | None,
        "The unique identifier for the customer payment you wish to update custom fields for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_custom_fields_in_customer_payment'."
]:
    """Update custom fields in existing customer payments.

    Use this tool to update the values of custom fields in an existing customer payment in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECUSTOMERPAYMENTCUSTOMFIELDS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not customer_payment_identifier:
        missing_params.append(("customer_payment_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMERPAYMENTCUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMERPAYMENTCUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/customerpayment/{customer_payment_id}/customfields".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            customer_payment_id=customer_payment_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECUSTOMERPAYMENTCUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_customer_payment_refund_details(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID of the organization. This is required to specify which organization's data to access.",  # noqa: E501
    ],
    customer_payment_unique_id: Annotated[
        str, "Unique identifier of the customer payment to retrieve refund details."
    ],
    refund_identifier: Annotated[
        str, "Unique identifier of the refund for the specified customer payment."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_customer_payment_refund'."]:
    """Obtain details of a specific customer payment refund.

    Use this tool to get detailed information about a particular refund related to a customer's payment."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/customerpayments/{customer_payment_id}/refunds/{refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            customer_payment_id=customer_payment_unique_id,
            refund_id=refund_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_payment_refund(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. Required to access the organization's data.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    customer_payment_identifier: Annotated[
        str | None,
        "Unique identifier of the customer payment to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    refund_identifier: Annotated[
        str | None,
        "Unique identifier for the refund transaction to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_customer_payment_refund'."]:
    """Update details of a customer payment refund.

    This tool updates the details of a refunded transaction for a customer payment in Zoho Books. It should be called when you need to modify existing refund details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEPAYMENTREFUND_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not customer_payment_identifier:
        missing_params.append(("customer_payment_identifier", "path"))
    if not refund_identifier:
        missing_params.append(("refund_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPAYMENTREFUND_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPAYMENTREFUND_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/customerpayments/{customer_payment_id}/refunds/{refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            customer_payment_id=customer_payment_identifier,
            refund_id=refund_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPAYMENTREFUND_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.customerpayments.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_customer_payment_refund(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the refund deletion is requested."
    ],
    customer_payment_identifier: Annotated[
        str,
        "The unique identifier for the customer payment associated with the refund to be deleted.",
    ],
    refund_identifier: Annotated[str, "Unique identifier of the refund to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_customer_payment_refund'."]:
    """Delete a refund for an existing customer payment.

    This tool should be called when you need to delete a refund associated with an existing customer payment in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/customerpayments/{customer_payment_id}/refunds/{refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            customer_payment_id=customer_payment_identifier,
            refund_id=refund_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_customer_estimate(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the estimate is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    send_estimate_to_contact: Annotated[
        bool | None,
        "Set to true to send the estimate to the contact person(s) associated with it, false to skip sending.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    ignore_automatic_estimate_number_generation: Annotated[
        bool | None,
        "Set to true to bypass automatic estimate number generation. This requires specifying an estimate number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_estimate'."]:
    """Create an estimate for a customer using Zoho Books.

    Use this tool to generate a new estimate for a customer within the Zoho Books system. This could be useful when users need to provide potential price quotes or service costs to clients.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATECUSTOMERESTIMATE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMERESTIMATE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMERESTIMATE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECUSTOMERESTIMATE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "send": send_estimate_to_contact,
            "ignore_auto_number_generation": ignore_automatic_estimate_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_estimates(
    context: ToolContext,
    organization_id: Annotated[
        str, "Specifies the ID of the organization to filter the estimates."
    ],
    estimate_number_filter: Annotated[
        str | None,
        "Specify an estimate number or use variants like 'startswith' or 'contains' for partial matching to filter estimates.",  # noqa: E501
    ] = None,
    reference_number_filter: Annotated[
        str | None,
        "Filter or search estimates by their reference number. Supports partial matches using 'startswith' and 'contains'.",  # noqa: E501
    ] = None,
    customer_name: Annotated[
        str | None,
        "Search estimates by customer's name with optional variants for partial matches, such as 'startswith' and 'contains'.",  # noqa: E501
    ] = None,
    total_filter: Annotated[
        float | None,
        "Specify a condition to search estimates by their total amount. Use variants like 'less_than', 'less_equals', 'greater_than', and 'greater_equals' for range queries.",  # noqa: E501
    ] = None,
    filter_by_customer_id: Annotated[
        str | None,
        "Filter or search estimates using the unique customer ID. Use the `customer_id` provided by the Contacts API for the same organization to retrieve estimates linked to a specific customer.",  # noqa: E501
    ] = None,
    filter_by_item_id: Annotated[
        str | None,
        "Filter or search estimates by the unique item ID. Use the item_id returned by the Items API for the same organization to find estimates including a specific product or service.",  # noqa: E501
    ] = None,
    item_name_filter: Annotated[
        str | None,
        "Search estimates by item name. Supports variants like 'item_name_startswith' and 'item_name_contains' for partial matches.",  # noqa: E501
    ] = None,
    search_by_item_description: Annotated[
        str | None,
        "Search estimates by item description. Use variants 'item_description_startswith' and 'item_description_contains' for pattern matching.",  # noqa: E501
    ] = None,
    search_by_custom_field: Annotated[
        str | None,
        "Search estimates by a custom field, supporting variants like 'startswith' or 'contains' for partial matches. Useful for identifying estimates linked to specific custom data.",  # noqa: E501
    ] = None,
    expiry_date: Annotated[
        str | None,
        "Specify the expiration date of the estimates to filter the results. Use the format YYYY-MM-DD.",  # noqa: E501
    ] = None,
    estimate_date_filter: Annotated[
        str | None,
        "Search estimates by date using variants like 'date_start', 'date_end', 'date_before', or 'date_after'.",  # noqa: E501
    ] = None,
    estimate_status_filter: Annotated[
        str | None,
        "Filter estimates by status. Allowed values: draft, sent, invoiced, accepted, declined, expired.",  # noqa: E501
    ] = None,
    filter_estimates_by_status: Annotated[
        str | None,
        "Specify the status to filter estimates. Allowed values: Status.All, Status.Sent, Status.Draft, Status.Invoiced, Status.Accepted, Status.Declined, Status.Expired.",  # noqa: E501
    ] = None,
    keyword_search: Annotated[
        str | None,
        "Keyword search across estimate number, reference number, or customer name to quickly find matching estimates.",  # noqa: E501
    ] = None,
    sort_estimates_by_column: Annotated[
        str | None,
        "Specify the column to sort estimates by. Options: customer_name, estimate_number, date, total, created_time.",  # noqa: E501
    ] = None,
    deal_potential_id: Annotated[
        int | None,
        "Potential ID of a Deal in CRM. Use this to filter estimates linked to specific deals.",
    ] = None,
    page_number: Annotated[int | None, "Specify the page number to fetch. Default is 1."] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_estimates'."]:
    """Retrieve a list of all estimates with pagination.

    Use this tool to fetch all estimates, including pagination information, from Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "estimate_number": estimate_number_filter,
            "reference_number": reference_number_filter,
            "customer_name": customer_name,
            "total": total_filter,
            "customer_id": filter_by_customer_id,
            "item_id": filter_by_item_id,
            "item_name": item_name_filter,
            "item_description": search_by_item_description,
            "custom_field": search_by_custom_field,
            "expiry_date": expiry_date,
            "date": estimate_date_filter,
            "status": estimate_status_filter,
            "filter_by": filter_estimates_by_status,
            "search_text": keyword_search,
            "sort_column": sort_estimates_by_column,
            "zcrm_potential_id": deal_potential_id,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_estimate_with_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Provide the ID of the organization for which the estimate is being updated or created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "The API name of the custom field used to uniquely identify and update an estimate.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "The unique value of the custom field used to identify and update the estimate.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_new_estimate_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new estimate if no existing record matches the custom field value.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_estimate_using_custom_field'."
]:
    """Update or create an estimate using a custom field value.

    This tool allows updating an existing estimate by providing a unique custom field value. If the value does not match any existing estimates and the X-Upsert header is true, a new estimate will be created if required details are provided. Use this tool to maintain estimates based on unique identifiers.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEESTIMATEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEESTIMATEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": create_new_estimate_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_estimate(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization whose estimate needs updating. It should be a unique identifier in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    estimate_unique_id: Annotated[
        str | None,
        "Unique identifier for the estimate you want to update in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to ignore auto generation of estimate numbers and manually specify the estimate number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_estimate'."]:
    """Update an existing estimate in Zoho Books.

    Use this tool to modify an existing estimate in Zoho Books. When updating, you can remove a line item by excluding it from the line_items list.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEESTIMATE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not estimate_unique_id:
        missing_params.append(("estimate_unique_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATEESTIMATE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATEESTIMATE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_unique_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEESTIMATE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "ignore_auto_number_generation": ignore_auto_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_estimate_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization in Zoho Books. Required to retrieve estimate details.",  # noqa: E501
    ],
    estimate_id: Annotated[
        str, "Unique identifier of the specific estimate to retrieve details for."
    ],
    response_format: Annotated[
        str | None,
        "Specify the format for the estimate details: json, pdf, or html. Default is json.",
    ] = None,
    print_pdf: Annotated[
        bool | None, "Set to true to print the exported PDF of the estimate."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_estimate'."]:
    """Retrieve the details of a specific estimate.

    Use this tool to get information about a particular estimate by providing its ID. It returns the details associated with the specified estimate in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "print": print_pdf,
            "accept": response_format,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_estimate(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization where the estimate will be deleted."
    ],
    estimate_id: Annotated[str, "Unique identifier for the estimate to be deleted in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_estimate'."]:
    """Delete an existing estimate in Zoho Books.

    Use this tool to remove an estimate by providing its unique estimate ID in Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_estimate_custom_fields(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization whose estimate custom fields are being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    estimate_identifier: Annotated[
        str | None,
        "Unique identifier for the estimate to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_custom_fields_in_estimate'."
]:
    """Update custom fields in a specific estimate.

    This tool updates the values of custom fields in an existing estimate. It should be called when you need to modify or add information to custom fields of a particular estimate.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEESTIMATECUSTOMFIELDS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not estimate_identifier:
        missing_params.append(("estimate_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATECUSTOMFIELDS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATECUSTOMFIELDS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimate/{estimate_id}/customfields".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEESTIMATECUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_estimate_as_sent(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. Required to identify which organization's estimate to mark as sent.",  # noqa: E501
    ],
    estimate_identifier: Annotated[
        str, "The unique identifier for the estimate to be marked as sent."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_estimate_sent'."]:
    """Mark a draft estimate as sent.

    Use this tool to mark a draft estimate in Zoho Books as sent. Call this when you need to update the status of an estimate to indicate it has been sent to a client."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/status/sent".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def accept_estimate(
    context: ToolContext,
    organization_id: Annotated[str, "ID for the organization related to the estimate acceptance."],
    estimate_identifier: Annotated[
        str, "Unique identifier of the estimate to be marked as accepted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_estimate_accepted'."]:
    """Mark a sent estimate as accepted if the customer has accepted it.

    Use this tool to update the status of a sent estimate to accepted once your customer has approved it."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/status/accepted".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def decline_estimate(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization for which the estimate is being declined."
    ],
    estimate_identifier: Annotated[str, "Unique identifier of the estimate to mark as declined."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_estimate_declined'."]:
    """Marks a sent estimate as declined if rejected by customer.

    Use this tool to update the status of an estimate to 'declined' when a customer has rejected it. This is helpful in maintaining accurate records of customer interactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/status/declined".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def submit_estimate_for_approval(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books that the estimate belongs to.",
    ],
    estimate_identifier: Annotated[
        str, "Unique identifier of the estimate to be submitted for approval."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'submit_estimate'."]:
    """Submit an estimate for approval.

    Use this tool to submit a specific estimate for approval by providing the estimate ID. It facilitates the approval workflow for estimates in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/submit".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def approve_estimate(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization in Zoho Books whose estimate is being approved. This should be the unique identifier associated with the organization.",  # noqa: E501
    ],
    estimate_identifier: Annotated[
        str, "Unique identifier for the estimate to be approved in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approve_estimate'."]:
    """Approve an estimate in Zoho Books.

    Use this tool to approve an estimate in the Zoho Books system when you have the estimate ID. It should be called when an estimate needs to be confirmed as approved."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/approve".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_estimate_email(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization. This ID is required to send an estimate email.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    estimate_identifier: Annotated[
        str | None,
        "Unique identifier of the estimate to be emailed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    email_attachments: Annotated[
        str | None,
        "Files to be attached to the email estimate. Provide file paths or URLs.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_estimate'."]:
    """Send an email estimate to a customer.

    Use this tool to email an estimate to a customer. If no specific content is provided, the default email content will be used.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["SENDESTIMATEEMAIL_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not estimate_identifier:
        missing_params.append(("estimate_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDESTIMATEEMAIL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDESTIMATEEMAIL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["SENDESTIMATEEMAIL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "attachments": email_attachments,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_estimate_email_content(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which you want to retrieve the estimate email content."
    ],
    email_template_id: Annotated[
        str,
        "Optional. Specify a template ID to retrieve the email content based on a specific template. If not provided, defaults to the customer's associated or default template.",  # noqa: E501
    ],
    estimate_id: Annotated[
        str, "Unique identifier for the estimate to retrieve its email content."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_estimate_email'."]:
    """Retrieve the email content for a specific estimate.

    Use this tool to get the email content associated with a specific estimate by providing the estimate ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "email_template_id": email_template_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_estimates_email(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    estimate_ids_to_email: Annotated[
        str, "Comma-separated string of up to 10 estimate IDs to send via email."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_multiple_estimates'."]:
    """Send multiple estimates to customers via email.

    Use this tool to email up to 10 estimates to your customers efficiently. Ideal for managing and sharing multiple estimates at once."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_id,
            "estimate_ids": estimate_ids_to_email,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def export_estimates_as_pdf(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. Required to specify which organization's estimates are to be exported.",  # noqa: E501
    ],
    estimate_ids: Annotated[
        str,
        "Comma-separated list of estimate IDs to include in the PDF. Maximum of 25 IDs allowed.",
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_export_estimates_as_pdf'."]:
    """Export up to 25 estimates as a single PDF document.

    Use this tool to export multiple estimates (up to 25) in one consolidated PDF document. Ideal for compiling estimates quickly into a single file for review or sharing."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/pdf".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "estimate_ids": estimate_ids,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def export_and_print_estimates(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization whose estimates are to be exported and printed."
    ],
    estimate_ids_to_export: Annotated[
        str, "Comma-separated list of estimate IDs to export and print. Maximum of 25 IDs allowed."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_print_estimates'."]:
    """Export and print estimates as a PDF file.

    Use this tool to export up to 25 estimates as a PDF file and prepare them for printing."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/print".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "estimate_ids": estimate_ids_to_export,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_estimate_billing_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization in Zoho Books. Required to specify the organization whose estimate billing address is to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    estimate_identifier: Annotated[
        str | None,
        "Unique identifier of the estimate to update the billing address.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_estimate_billing_address'."]:
    """Updates the billing address for a specific estimate.

    Use this tool to update the billing address for a specific estimate in Zoho Books. Call this tool when there's a need to change the billing details associated with an individual estimate.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEESTIMATEBILLINGADDRESS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not estimate_identifier:
        missing_params.append(("estimate_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATEBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATEBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/address/billing".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEESTIMATEBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_estimate_shipping_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization in Zoho Books whose estimate's shipping address is to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    estimate_identifier: Annotated[
        str | None,
        "Unique identifier of the estimate to update its shipping address.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_estimate_shipping_address'."
]:
    """Updates the shipping address for an existing estimate in Zoho Books.

    This tool is used to update the shipping address for a specific estimate in the Zoho Books system. It should be called when there's a need to modify the delivery details associated with an existing estimate.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEESTIMATESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not estimate_identifier:
        missing_params.append(("estimate_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/address/shipping".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEESTIMATESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_estimate_templates(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to fetch estimate templates."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_estimate_templates'."]:
    """Retrieve all estimate PDF templates.

    This tool retrieves a list of all available PDF templates used for estimates. It should be called when users need to view or select templates for estimate documents."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/templates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_estimate_template(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization for which the estimate template is being updated."
    ],
    estimate_identifier: Annotated[
        str, "Provide the unique identifier for the specific estimate you want to update."
    ],
    estimate_template_identifier: Annotated[
        str, "Unique identifier for the estimate template to update in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_estimate_template'."]:
    """Update the PDF template for an estimate.

    Use this tool to update the PDF template associated with a specific estimate in Zoho Books by providing the estimate and template IDs."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/templates/{template_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            estimate_id=estimate_identifier,
            template_id=estimate_template_identifier,
        ),
        method="PUT",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_estimate_comments(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization whose estimate comments are needed."
    ],
    estimate_identifier: Annotated[
        str, "Unique identifier for the estimate to retrieve its history and comments."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_estimate_comments'."]:
    """Get the complete history and comments of an estimate.

    This tool retrieves all comments and historical data associated with a specific estimate. Use it to track changes or discussions about an estimate."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_estimate_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization within Zoho Books. It is required to specify which organization's estimate is being commented on.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    estimate_identifier: Annotated[
        str | None,
        "Unique identifier for the specific estimate to comment on.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_estimate_comment'."]:
    """Add a comment for a specific estimate in Zoho Books.

    Use this tool to add a comment to an existing estimate in Zoho Books. Call it when you need to append notes or feedback to an estimate record.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDESTIMATECOMMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not estimate_identifier:
        missing_params.append(("estimate_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDESTIMATECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDESTIMATECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), estimate_id=estimate_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDESTIMATECOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_estimate_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The ID representing the organization. Required to update the comment in the specified organization's estimate.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    estimate_identifier: Annotated[
        str | None,
        "Unique identifier of the estimate to update the comment for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    comment_unique_identifier: Annotated[
        str | None,
        "The unique identifier of the comment to be updated on an estimate.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_estimate_comment'."]:
    """Update an existing comment on an estimate.

    Use this tool to update a specific comment associated with an estimate in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEESTIMATECOMMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not estimate_identifier:
        missing_params.append(("estimate_identifier", "path"))
    if not comment_unique_identifier:
        missing_params.append(("comment_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEESTIMATECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            estimate_id=estimate_identifier,
            comment_id=comment_unique_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEESTIMATECOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.estimates.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_estimate_comment(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization where the estimate comment is located."
    ],
    estimate_unique_id: Annotated[
        str,
        "The unique identifier for the estimate, required to specify which estimate's comment to delete.",  # noqa: E501
    ],
    comment_unique_identifier: Annotated[str, "Unique identifier of the comment to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_estimate_comment'."]:
    """Delete an estimate comment.

    Use this tool to remove a comment from an estimate in Zoho Books. Ideal for cleaning up or managing comments related to estimates."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/estimates/{estimate_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            estimate_id=estimate_unique_id,
            comment_id=comment_unique_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_expense(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the expense is being recorded.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    expense_receipt_file: Annotated[
        str | None,
        "File path or URL for the expense receipt. Accepted formats: gif, png, jpeg, jpg, bmp, pdf, xls, xlsx, doc, docx.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_expense'."]:
    """Create a billable or non-billable expense record.

    Use this tool to create an expense entry that can be marked as billable or non-billable. Ideal for tracking expenses in your financial system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEEXPENSE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATEEXPENSE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATEEXPENSE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/expenses".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEEXPENSE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "receipt": expense_receipt_file,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_expenses(
    context: ToolContext,
    organization_id: Annotated[
        str, "Unique identifier for the organization whose expenses are being queried."
    ],
    search_by_description: Annotated[
        str | None,
        "Search expenses by description, supports 'description_startswith' and 'description_contains'. Max-length is 100 characters.",  # noqa: E501
    ] = None,
    search_by_reference_number: Annotated[
        str | None,
        "Search expenses by part or complete reference number using 'startswith' or 'contains'. Max-length is 100 characters.",  # noqa: E501
    ] = None,
    filter_by_date: Annotated[
        str | None,
        "Search expenses by expense date. Use variants: date_start, date_end, date_before, or date_after. Format [yyyy-mm-dd].",  # noqa: E501
    ] = None,
    expense_status: Annotated[
        str | None,
        "Search expenses by status. Allowed values: unbilled, invoiced, reimbursed, non-billable, billable.",  # noqa: E501
    ] = None,
    amount_filter: Annotated[
        float | None,
        "Search expenses by amount using the variants: less_than, less_equals, greater_than, or greater_equals.",  # noqa: E501
    ] = None,
    search_expense_account_name: Annotated[
        str | None,
        "Search expenses by account name. Use 'startswith:' or 'contains:'. Max length is 100 characters.",  # noqa: E501
    ] = None,
    customer_name_filter: Annotated[
        str | None,
        "Filter expenses by customer name. Supports 'startswith' and 'contains' variants. Max length is 100 characters.",  # noqa: E501
    ] = None,
    vendor_name_filter: Annotated[
        str | None,
        "Filter expenses by vendor name using 'vendor_name_startswith' or 'vendor_name_contains'.",
    ] = None,
    expense_account_customer_id: Annotated[
        str | None,
        "The ID of the expense account for the customer. Use this to filter expenses specific to a customer's account.",  # noqa: E501
    ] = None,
    vendor_id: Annotated[str | None, "ID of the vendor associated with the expense."] = None,
    recurring_expense_id: Annotated[
        str | None, "The ID used to search for expenses associated with a recurring expense."
    ] = None,
    paid_through_account_id: Annotated[
        str | None, "The ID of the account through which the expense was paid."
    ] = None,
    search_expenses_text: Annotated[
        str | None,
        "Search expenses by account name, description, customer name, or vendor name. Maximum length is 100 characters.",  # noqa: E501
    ] = None,
    sort_expenses_by: Annotated[
        str | None,
        "Sort expenses by the specified column. Allowed values: date, account_name, total, bcy_total, reference_number, customer_name, created_time.",  # noqa: E501
    ] = None,
    expense_status_filter: Annotated[
        str | None,
        "Filter expenses by status. Allowed values: 'Status.All', 'Status.Billable', 'Status.Nonbillable', 'Status.Reimbursed', 'Status.Invoiced', 'Status.Unbilled'.",  # noqa: E501
    ] = None,
    page_number: Annotated[int | None, "Page number to fetch, with the default starting at 1."] = 1,
    records_per_page: Annotated[
        int | None, "Number of expense records to fetch per page. Defaults to 200 if not specified."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_expenses'."]:
    """Retrieve a list of expenses with pagination.

    Use this tool to get a paginated list of all expenses recorded in the system."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/expenses".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "description": search_by_description,
            "reference_number": search_by_reference_number,
            "date": filter_by_date,
            "status": expense_status,
            "amount": amount_filter,
            "account_name": search_expense_account_name,
            "customer_name": customer_name_filter,
            "vendor_name": vendor_name_filter,
            "customer_id": expense_account_customer_id,
            "vendor_id": vendor_id,
            "recurring_expense_id": recurring_expense_id,
            "paid_through_account_id": paid_through_account_id,
            "search_text": search_expenses_text,
            "sort_column": sort_expenses_by,
            "filter_by": expense_status_filter,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_expense_with_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization for which the expense update is intended. It is required to identify the target organization in the API.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_field_api_name: Annotated[
        str | None,
        "API name of the unique custom field used to identify the expense.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "The unique value for the custom field used to update or create an expense.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    allow_upsert_new_expense: Annotated[
        bool | None,
        "Set to true to create a new expense if no matching unique custom field value is found.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_expense_using_custom_field'."
]:
    """Update or create an expense using custom field values.

    This tool updates an existing expense based on a unique custom field value. If the unique value doesn't match any existing expenses, and the X-Upsert header is true, a new expense will be created if all required details are provided.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEEXPENSEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEEXPENSEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEEXPENSEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/expenses".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEEXPENSEWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": allow_upsert_new_expense,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_existing_expense(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization. Required to identify which organization's expense is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    expense_identifier: Annotated[
        str | None,
        "The unique identifier for the expense to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    expense_receipt_file: Annotated[
        str | None,
        "File path of the expense receipt to attach. Allowed extensions are gif, png, jpeg, jpg, bmp, pdf, xls, xlsx, doc, and docx. Ensure the file is accessible and in an accepted format.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    delete_receipt: Annotated[
        bool | None,
        "Set to true to remove the attached receipt from the expense. Use false to keep it.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_expense'."]:
    """Update an existing expense in Zoho Books.

    Use this tool to modify details of an existing expense in Zoho Books. It should be called when you need to change information such as amount, date, or description of a recorded expense.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEEXISTINGEXPENSE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not expense_identifier:
        missing_params.append(("expense_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEEXISTINGEXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEEXISTINGEXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/expenses/{expense_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), expense_id=expense_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEEXISTINGEXPENSE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "receipt": expense_receipt_file,
            "delete_receipt": delete_receipt,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_expense_details(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "String representing the ID of the organization for which the expense details are requested.",  # noqa: E501
    ],
    expense_identifier: Annotated[
        str, "Unique identifier for the expense to retrieve its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_expense'."]:
    """Retrieve details of a specific expense by ID.

    Use this tool to get detailed information about an expense by providing the expense ID. Ideal for financial reviews or tracking specific expenses."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/expenses/{expense_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), expense_id=expense_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_expense_entry(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique identifier for the organization within Zoho Books."
    ],
    expense_identifier: Annotated[
        str, "Unique identifier of the expense to be deleted in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_expense'."]:
    """Delete an existing expense entry in Zoho Books.

    Use this tool to delete a specific expense entry from Zoho Books when you need to remove an incorrect or unnecessary record."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/expenses/{expense_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), expense_id=expense_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_expense_comments(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization in Zoho Books required to fetch the expense comments.",  # noqa: E501
    ],
    expense_unique_id: Annotated[
        str, "Unique identifier for the expense to retrieve its comments and history in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_expense_comments'."]:
    """Retrieve comments and history for a specific expense.

    This tool should be called to get the discussion and history associated with an expense, specified by its ID, in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/expenses/{expense_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), expense_id=expense_unique_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_company_employees(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization whose employees are being listed. This should be provided as a string.",  # noqa: E501
    ],
    page_number: Annotated[
        int | None, "The page number to fetch. Default is 1 for the first page."
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Specify the number of employee records to retrieve per page, with a default of 200.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_employees'."]:
    """Retrieve a paginated list of all employees.

    Use this tool to obtain a list of employees from the company database, including pagination for handling large sets of data. Ideal for getting employee details in an organized manner."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/employees".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_employee_for_expense(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books where the employee will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_employee'."]:
    """Create an employee for an expense record in Zoho Books.

    This tool is used to create a new employee entry for the purpose of logging an expense in Zoho Books. It should be called when there is a need to register an employee related to specific expenses.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATEEMPLOYEEFOREXPENSE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEEMPLOYEEFOREXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEEMPLOYEEFOREXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/employees".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEEMPLOYEEFOREXPENSE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def fetch_employee_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization in Zoho Books. This ID is necessary to specify which organization's employee details are being requested.",  # noqa: E501
    ],
    employee_unique_id: Annotated[
        str, "The unique identifier for the employee whose details are to be fetched in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_employee'."]:
    """Retrieve detailed information about an employee.

    This tool is used to obtain comprehensive details of a specific employee by their ID in Zoho Books. It should be called when detailed employee information is needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/employees/{employee_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), employee_id=employee_unique_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_employee_record(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "ID of the organization to uniquely identify it for employee deletion."
    ],
    employee_identifier: Annotated[
        str, "The unique identifier for the employee to be deleted in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_employee'."]:
    """Remove an employee from the records in Zoho Books.

    This tool is used to delete an existing employee in Zoho Books. It should be called when there is a need to permanently remove an employee's record from the system."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/employee/{employee_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), employee_id=employee_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_expense_receipt(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. Required to retrieve the expense receipt.",  # noqa: E501
    ],
    expense_identifier: Annotated[
        str,
        "Unique identifier for the expense to retrieve its receipt. Required for locating the specific expense in Zoho Books.",  # noqa: E501
    ],
    get_receipt_thumbnail: Annotated[
        bool | None,
        "Set to true to get a thumbnail of the receipt; false returns the full receipt.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_expense_receipt'."]:
    """Retrieve the receipt attached to an expense.

    Use this tool to obtain the receipt attached to a specific expense entry in Zoho Books. It is useful for reviewing or auditing expense details by accessing the actual receipt document."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/expenses/{expense_id}/receipt".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), expense_id=expense_identifier
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "preview": get_receipt_thumbnail,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def attach_expense_receipt(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    expense_id: Annotated[
        str, "Unique identifier for the expense to which the receipt will be attached."
    ],
    expense_receipt_file: Annotated[
        str | None,
        "The file to attach as an expense receipt. Supported formats: gif, png, jpeg, jpg, bmp, pdf, xls, xlsx, doc, docx.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_expense_receipt'."]:
    """Attach a receipt to a specified expense.

    Use this tool to attach a receipt file to an existing expense record, identified by the expense ID. It is helpful when you need to provide proof or documentation for expense entries in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/expenses/{expense_id}/receipt".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), expense_id=expense_id
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_id,
            "receipt": expense_receipt_file,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_expense_receipt(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization for which the expense receipt is to be deleted. Ensure it's accurate to target the correct entity.",  # noqa: E501
    ],
    expense_id: Annotated[str, "Unique identifier for the expense whose receipt is to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_expense_receipt'."]:
    """Deletes the receipt attached to an expense.

    Use this tool to remove the receipt associated with a specific expense in Zoho Books, identified by the expense ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/expenses/{expense_id}/receipt".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), expense_id=expense_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_fixed_asset(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books for which the fixed asset is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_fixed_asset'."]:
    """Create a fixed asset in Zoho Books.

    This tool is used to create a fixed asset in the Zoho Books platform. It should be called when a user wants to register a new fixed asset in their accounting records.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEFIXEDASSET_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATEFIXEDASSET_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATEFIXEDASSET_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/fixedassets".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEFIXEDASSET_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_fixed_assets_list(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization for which to list fixed assets."
    ],
    filter_fixed_asset_status: Annotated[
        str | None,
        "Filter the fixed asset list by status. Valid inputs: Status.All, Status.Active, Status.Cancel, Status.FullyDepreciated, Status.WriteOff, Status.Sold, Status.Draft.",  # noqa: E501
    ] = None,
    sort_by_column: Annotated[
        str | None,
        "Specify the column to sort the fixed asset list. Choose from: asset_name, asset_number, asset_cost, created_time, current_asset_value.",  # noqa: E501
    ] = None,
    sort_order: Annotated[
        str | None,
        "Sort the fixed asset list in ascending or descending order. Use 'A' for ascending and 'D' for descending.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None,
        "The page number to fetch from the fixed asset list. Defaults to 1 if not specified.",
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of fixed asset records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_fixed_assets'."]:
    """Retrieve a list of fixed assets from Zoho Books.

    Use this tool to obtain a detailed list of fixed assets stored in Zoho Books. Ideal for managing and reviewing asset inventories."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "filter_by": filter_fixed_asset_status,
            "sort_column": sort_by_column,
            "sort_order": sort_order,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_fixed_asset_info(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization whose fixed asset you wish to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    fixed_asset_identifier: Annotated[
        str | None,
        "Unique identifier for the specific fixed asset to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_fixed_asset'."]:
    """Update fixed asset details in Zoho Books.

    Use this tool to update the information of a specific fixed asset in Zoho Books by providing the necessary asset details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEFIXEDASSETINFO_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not fixed_asset_identifier:
        missing_params.append(("fixed_asset_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEFIXEDASSETINFO_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEFIXEDASSETINFO_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEFIXEDASSETINFO_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_fixed_asset_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "Provide the ID of the organization for which you want to retrieve the asset details."
    ],
    fixed_asset_identifier: Annotated[
        str, "Unique identifier for the fixed asset to retrieve its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_fixed_asset'."]:
    """Retrieve details of a fixed asset using its ID.

    This tool is used to obtain detailed information about a fixed asset by providing its unique ID. It should be called when you need specific details about an asset in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_fixed_asset(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization from which the fixed asset will be deleted. Ensure this ID corresponds to the correct organization.",  # noqa: E501
    ],
    fixed_asset_identifier: Annotated[
        str,
        "Unique identifier of the fixed asset to be deleted. This ID is required to specify which asset to remove.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_fixed_asset'."]:
    """Delete a specified fixed asset.

    Use this tool to delete a fixed asset by providing its ID. This action is irreversible, so ensure you have the correct ID before proceeding."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def fetch_asset_history(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization whose asset history is being requested."
    ],
    fixed_asset_identifier: Annotated[
        str, "Unique identifier for the fixed asset. Required to fetch its detailed history."
    ],
    page_number: Annotated[int | None, "Page number to retrieve, with a default value of 1."] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Defaults to 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_fixed_asset_history'."]:
    """Fetch the detailed history of a specific fixed asset.

    This tool retrieves a comprehensive history of a fixed asset, detailing its journey from acquisition to write-off. It should be called when you need insights into a fixed asset's lifecycle."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/history".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_asset_depreciation_summary(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID of the organization for which the asset's future depreciation rates are to be retrieved.",  # noqa: E501
    ],
    fixed_asset_identifier: Annotated[
        str, "Unique identifier for the fixed asset to fetch its future depreciation rates."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_fixed_asset_forecast'."]:
    """Displays detailed future depreciation rates for a fixed asset.

    Use this tool to obtain a summary of an asset's future depreciation rates. It is helpful for financial forecasting and decision-making regarding fixed assets."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/forecast".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_fixed_asset(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "ID of the organization. This is required to identify which organization's asset to activate.",  # noqa: E501
    ],
    fixed_asset_id: Annotated[
        str, "Unique identifier of the fixed asset to activate for depreciation calculation."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_fixed_asset_active'."]:
    """Activate a fixed asset to begin depreciation calculation.

    Use this tool to mark a fixed asset as active, which will initiate the calculation of its depreciation."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/status/active".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), fixed_asset_id=fixed_asset_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def cancel_fixed_asset(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. Required to specify which organization's fixed asset to cancel.",  # noqa: E501
    ],
    fixed_asset_id: Annotated[str, "Unique identifier for the fixed asset to be canceled."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_fixed_asset_cancel'."]:
    """Cancel a fixed asset in Zoho Books.

    Use this tool to cancel a fixed asset in Zoho Books by providing the fixed asset ID. This changes the status of the asset to canceled."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/status/cancel".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), fixed_asset_id=fixed_asset_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_fixed_asset_as_draft(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization for which the fixed asset status will be changed."
    ],
    fixed_asset_identifier: Annotated[str, "Unique identifier for the fixed asset in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_fixed_asset_draft'."]:
    """Set a fixed asset status to draft in Zoho Books.

    Use this tool to change the status of a specific fixed asset to draft in Zoho Books. This is useful when revising asset details or temporarily removing it from active use."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/status/draft".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def write_off_fixed_asset(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Required to specify which organization's asset is to be written off.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    fixed_asset_identifier: Annotated[
        str | None,
        "Unique identifier of the fixed asset to be written off in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'write_off_fixed_asset'."]:
    """Remove a fixed asset from the records.

    Use this tool to write off a fixed asset. It should be called when an asset needs to be removed from the records in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["WRITEOFFFIXEDASSET_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not fixed_asset_identifier:
        missing_params.append(("fixed_asset_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["WRITEOFFFIXEDASSET_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["WRITEOFFFIXEDASSET_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/writeoff".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["WRITEOFFFIXEDASSET_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def sell_fixed_asset(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization within Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    fixed_asset_identifier: Annotated[
        str | None,
        "Unique identifier of the fixed asset to be sold.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'sell_fixed_asset'."]:
    """Initiate the sale of a specified fixed asset.

    Use this tool to sell a specific fixed asset by providing its asset ID. It should be called when confirming or completing the sale process of a fixed asset in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["SELLFIXEDASSET_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not fixed_asset_identifier:
        missing_params.append(("fixed_asset_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["SELLFIXEDASSET_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["SELLFIXEDASSET_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/sell".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["SELLFIXEDASSET_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_fixed_asset_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books. This is required to add a comment to the fixed asset.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    fixed_asset_identifier: Annotated[
        str | None,
        "Unique identifier for the fixed asset to add a comment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_fixed_asset_comment'."]:
    """Add a comment to a fixed asset in Zoho Books.



    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDFIXEDASSETCOMMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not fixed_asset_identifier:
        missing_params.append(("fixed_asset_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDFIXEDASSETCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDFIXEDASSETCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDFIXEDASSETCOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_asset_comment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "Provide the ID of the organization to specify which organization's asset comment you want to delete.",  # noqa: E501
    ],
    fixed_asset_identifier: Annotated[
        str, "Unique identifier of the fixed asset to delete a comment from."
    ],
    comment_id: Annotated[
        str, "Unique identifier of the comment to be deleted from the fixed asset."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_fixed_asset_comment'."]:
    """Delete a comment from a fixed asset in Zoho Books.

    Use this tool to delete an existing comment on a fixed asset within Zoho Books by specifying the fixed asset and comment IDs."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassets/{fixed_asset_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_id=fixed_asset_identifier,
            comment_id=comment_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_fixed_asset_type(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which to create the fixed asset type.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_fixed_asset_type'."]:
    """Create a fixed asset type in Zoho Books.

    Use this tool to create a new fixed asset type in Zoho Books whenever you need to categorize assets. It facilitates asset management by defining specific asset categories.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEFIXEDASSETTYPE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEFIXEDASSETTYPE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEFIXEDASSETTYPE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/fixedassettypes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEFIXEDASSETTYPE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_fixed_asset_type_list(
    context: ToolContext,
    organization_id: Annotated[
        str, "Unique identifier for the organization to retrieve asset types for."
    ],
    page_number_to_fetch: Annotated[
        int | None,
        "The page number to retrieve for the list of fixed asset types. Defaults to 1 if not specified.",  # noqa: E501
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_fixed_asset_type_list'."]:
    """Retrieve a list of fixed asset types.

    Call this tool to obtain a list of all available fixed asset types in Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassettypes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_fixed_asset_type(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    fixed_asset_type_identifier: Annotated[
        str | None,
        "Unique identifier for the fixed asset type to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_fixed_asset_type'."]:
    """Update a fixed asset type with new information.

    Use this tool to update the details of a fixed asset type by providing the relevant asset type ID and new information. It should be called when modifying asset categories in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEFIXEDASSETTYPE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not fixed_asset_type_identifier:
        missing_params.append(("fixed_asset_type_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEFIXEDASSETTYPE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEFIXEDASSETTYPE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/fixedassettypes/{fixed_asset_type_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_type_id=fixed_asset_type_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEFIXEDASSETTYPE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.fixedasset.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_fixed_asset_type(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID of the organization within Zoho Books."],
    fixed_asset_type_identifier: Annotated[
        str, "Unique identifier for the fixed asset type to delete."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_fixed_asset_type'."]:
    """Deletes a specified fixed asset type from the system."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/fixedassettypes/{fixed_asset_type_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            fixed_asset_type_id=fixed_asset_type_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def import_customer_from_crm(
    context: ToolContext,
    organization_id: Annotated[str, "Unique identifier for the organization within Zoho Books."],
    crm_account_id: Annotated[
        str, "Unique identifier of the Zoho CRM account to import the customer from."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'import_customer_using_crm_account_id'."
]:
    """Import a customer from Zoho CRM to Zoho Books using CRM account ID.

    Use this tool to import a customer from Zoho CRM into Zoho Books by providing the CRM account ID. This requires integration between Zoho Books and Zoho CRM either through Accounts and Contacts sync or Accounts only sync."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/crm/account/{crm_account_id}/import".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), crm_account_id=crm_account_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def crm_to_books_contact_import(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization within Zoho Books."
    ],
    zoho_crm_contact_id: Annotated[
        str, "Unique identifier for the Zoho CRM contact to import into Zoho Books."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'import_customer_using_crm_contact_id'."
]:
    """Import a customer from Zoho CRM to Zoho Books using CRM contact ID.

    Use this tool when you need to import a customer from Zoho CRM to Zoho Books based on their CRM contact ID. Ensure that Zoho Books is integrated with Zoho CRM using the correct sync settings for contacts."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/crm/contact/{crm_contact_id}/import".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            crm_contact_id=zoho_crm_contact_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.contacts.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def import_vendor_from_crm(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization in Zoho Books for which the vendor is being imported. This ID is used to specify the target organization in Zoho Books.",  # noqa: E501
    ],
    zoho_crm_vendor_id: Annotated[str, "Unique identifier of the Zoho CRM vendor to import."],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'import_vendor_using_crm_vendor_id'."
]:
    """Import a vendor from Zoho CRM to Zoho Books using CRM vendor ID.

    Use this tool to import a vendor from Zoho CRM to Zoho Books via Vendor-only sync, requiring the CRM vendor ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/crm/vendor/{crm_vendor_id}/import".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), crm_vendor_id=zoho_crm_vendor_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def import_crm_product_to_zoho_books(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. This is required to import products from Zoho CRM.",  # noqa: E501
    ],
    crm_product_id: Annotated[
        str, "Unique identifier of the Zoho CRM product to be imported into Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint ''."]:
    """Import a product from Zoho CRM to Zoho Books.

    Use this tool to import an item from Zoho CRM into Zoho Books by specifying its CRM product ID. This enables synchronization of product data between the two services."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/crm/item/{crm_product_id}/import".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), crm_product_id=crm_product_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_customer_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the invoice is created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    send_invoice_to_contacts: Annotated[
        bool | None,
        "Boolean to determine if the invoice is sent to the contact persons. Use 'true' to send, 'false' otherwise.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to ignore auto invoice number generation, requiring manual input.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_quick_create_mode: Annotated[
        bool | None,
        "Enable quick create mode for streamlined invoice creation with minimal required fields. Set to true for activation.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    enable_batch_payments: Annotated[
        bool | None,
        "Enable batch payment processing for the invoice. True means the invoice is included in batch operations. Requires 'is_quick_create' to be true.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_invoice'."]:
    """Create an invoice for your customer.



    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATECUSTOMERINVOICE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMERINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATECUSTOMERINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATECUSTOMERINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "send": send_invoice_to_contacts,
            "ignore_auto_number_generation": ignore_auto_number_generation,
            "is_quick_create": enable_quick_create_mode,
            "batch_payments": enable_batch_payments,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_invoice_list(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization for which invoices are being queried. This ID is crucial for accessing the correct set of invoices within Zoho Books.",  # noqa: E501
    ],
    search_by_invoice_number: Annotated[
        str | None,
        "Search for invoices using their unique invoice number. Supports 'startswith' and 'contains' variants. Max length: 100 characters.",  # noqa: E501
    ] = None,
    search_invoice_by_item_name: Annotated[
        str | None,
        "Filters invoices by product or service name in line items. Supports 'item_name_startswith' and 'item_name_contains'. Max length: 100 characters.",  # noqa: E501
    ] = None,
    search_by_item_id: Annotated[
        str | None,
        "Search invoices by item ID. Use the unique identifier of a product or service to filter invoices that include specific line items.",  # noqa: E501
    ] = None,
    item_description_filter: Annotated[
        str | None,
        "Filter invoices by item description using keywords. Supports 'startswith' and 'contains' variants. Max 100 characters.",  # noqa: E501
    ] = None,
    search_reference_number: Annotated[
        str | None,
        "Search invoices by reference number, such as purchase order or project codes, to find invoices associated with specific projects or transactions.",  # noqa: E501
    ] = None,
    search_by_customer_name: Annotated[
        str | None,
        "Search for invoices using the customer's name, with a maximum length of 100 characters, to generate customer-specific reports or find all invoices for a customer.",  # noqa: E501
    ] = None,
    recurring_invoice_id: Annotated[
        str | None,
        "ID of the recurring invoice from which the invoice is created. Use to filter invoices tied to specific recurring billing cycles.",  # noqa: E501
    ] = None,
    customer_email_filter: Annotated[
        str | None,
        "Filter invoices by the customer's email address. Maximum length is 100 characters. Ideal for finding specific customer invoices or customer segment analysis.",  # noqa: E501
    ] = None,
    search_by_total_amount: Annotated[
        str | None,
        "Search and filter invoices based on the final total amount, including taxes, discounts, and adjustments. Useful for finding invoices within specific price ranges or identifying high-value transactions.",  # noqa: E501
    ] = None,
    search_by_outstanding_balance: Annotated[
        str | None,
        "Filter invoices by outstanding balance to find overdue invoices, track receivables, or generate aging reports.",  # noqa: E501
    ] = None,
    search_by_custom_field: Annotated[
        str | None,
        "Search invoices using custom fields. Supports 'custom_field_startswith' and 'custom_field_contains' for partial matching.",  # noqa: E501
    ] = None,
    invoice_date_filter: Annotated[
        str | None,
        "Filter invoices by invoice date using yyyy-mm-dd format. Supports variants like date_start, date_end, date_before, and date_after to find invoices within specific date ranges.",  # noqa: E501
    ] = None,
    invoice_due_date_filter: Annotated[
        str | None,
        "Filter invoices by due date using yyyy-mm-dd format. Supports start, end, before, and after variants for flexible searching.",  # noqa: E501
    ] = None,
    filter_by_creation_date: Annotated[
        str | None,
        "Filter invoices by creation date with yyyy-mm-dd format. Supports variants: start, end, before, and after.",  # noqa: E501
    ] = None,
    filter_by_last_modified_time: Annotated[
        str | None,
        "Filters invoices modified after a specific timestamp in YYYY-MM-DDTHH:MM:SS-UTC format. Useful for identifying recently updated invoices.",  # noqa: E501
    ] = None,
    invoice_status: Annotated[
        str | None, "Filter invoices by their current status (e.g., sent, draft, overdue, etc.)."
    ] = None,
    search_by_customer_id: Annotated[
        str | None,
        "Filters invoices using the unique customer ID. Use the ID from the Contacts API to find all invoices for a specific customer.",  # noqa: E501
    ] = None,
    filter_invoices_by_criteria: Annotated[
        str | None,
        "Filter invoices by status (e.g., Status.Sent, Status.Paid) or payment expected date using Date.PaymentExpectedDate.",  # noqa: E501
    ] = None,
    general_search_text: Annotated[
        str | None,
        "General search for invoices by invoice number, purchase order, or customer name. Accepts up to 100 characters.",  # noqa: E501
    ] = None,
    sort_by_column: Annotated[
        str | None,
        "Specify the column to sort invoices by. Options: customer_name, invoice_number, date, due_date, total, balance, created_time.",  # noqa: E501
    ] = None,
    search_by_crm_potential_id: Annotated[
        int | None,
        "Find invoices linked to a specific CRM deal or opportunity using its potential ID from Zoho CRM.",  # noqa: E501
    ] = None,
    response_format_type: Annotated[
        int | None,
        "Specifies the desired response format: 0 for all invoices, 1 for all invoices with counts and totals, 2 for count only, 3 for count and totals, 4 for invoices and totals.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None,
        "Page number to fetch from paginated results. Default is 1. Use with 'per_page' for navigating large data sets.",  # noqa: E501
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Number of records to fetch per page. Default is 200, maximum is 200. Use to control result size for performance optimization.",  # noqa: E501
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_invoices'."]:
    """Retrieve and organize a list of invoices from Zoho Books.

    This tool fetches a list of invoices using Zoho Books' API with options for pagination, filtering, searching, and sorting. Useful for finding specific invoices or browsing records systematically."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "invoice_number": search_by_invoice_number,
            "item_name": search_invoice_by_item_name,
            "item_id": search_by_item_id,
            "item_description": item_description_filter,
            "reference_number": search_reference_number,
            "customer_name": search_by_customer_name,
            "recurring_invoice_id": recurring_invoice_id,
            "email": customer_email_filter,
            "total": search_by_total_amount,
            "balance": search_by_outstanding_balance,
            "custom_field": search_by_custom_field,
            "date": invoice_date_filter,
            "due_date": invoice_due_date_filter,
            "created_date": filter_by_creation_date,
            "last_modified_time": filter_by_last_modified_time,
            "status": invoice_status,
            "customer_id": search_by_customer_id,
            "filter_by": filter_invoices_by_criteria,
            "search_text": general_search_text,
            "sort_column": sort_by_column,
            "zcrm_potential_id": search_by_crm_potential_id,
            "response_option": response_format_type,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_invoice_by_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. This is required to specify which organization's invoice should be updated or created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "The API name of the unique custom field used to locate the invoice to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_field_value: Annotated[
        str | None,
        "The unique value of the custom field used to find or create the invoice.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_new_invoice_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new invoice if the unique custom field value is not found in existing invoices.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_invoice_using_custom_field'."
]:
    """Update or create an invoice using a custom field value.

    Call this tool to update an existing invoice or create a new one using a custom field's unique value. If the value is not found and X-Upsert is true, a new invoice will be created if all required details are provided.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEINVOICEBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEINVOICEBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEINVOICEBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEINVOICEBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": custom_field_value,
            "X-Upsert": create_new_invoice_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization to which the invoice belongs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_id: Annotated[
        str | None,
        "Unique identifier of the invoice to be updated. Ensure this ID corresponds to an existing invoice.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    ignore_auto_invoice_number_generation: Annotated[
        bool | None,
        "Set to true to ignore automatic invoice number generation, requiring manual entry of the invoice number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_invoice'."]:
    """Update details of an existing invoice in Zoho Books.

    Use this tool to modify the details of an existing invoice. If you need to remove a line item, simply exclude it from the line_items list.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEINVOICE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not invoice_id:
        missing_params.append(("invoice_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATEINVOICE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATEINVOICE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "ignore_auto_number_generation": ignore_auto_invoice_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_invoice_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization for which the invoice details are requested."
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice. Used to specify which invoice details to retrieve."
    ],
    format_type: Annotated[
        str | None, "Specify the format for invoice details: json, pdf, or html. Default is json."
    ] = None,
    print_pdf: Annotated[
        bool | None, "Boolean value indicating whether to print the exported PDF."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_invoice'."]:
    """Retrieve details of a specific invoice by ID.

    This tool is used to get detailed information about a specific invoice using its ID. It can be called when needing to verify invoice details, check status, or confirm amounts."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "print": print_pdf,
            "accept": format_type,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_invoice_in_zoho_books(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the invoice is to be deleted in Zoho Books."
    ],
    invoice_identifier: Annotated[str, "Unique identifier of the invoice to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_invoice'."]:
    """Delete an existing invoice in Zoho Books.

    Use this tool to delete an existing invoice in Zoho Books. Note that invoices with payments or credit notes applied cannot be deleted."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_invoice_as_sent(
    context: ToolContext,
    organization_id: Annotated[
        str, "Unique identifier of the organization for which the invoice will be marked as sent."
    ],
    invoice_unique_identifier: Annotated[
        str, "Unique identifier of the invoice to be marked as sent."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_invoice_sent'."]:
    """Mark a draft invoice as sent.

    Use this tool to change the status of a draft invoice to 'sent'. Ideal for when an invoice is ready to be sent out to a client."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/status/sent".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_unique_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def void_invoice_status(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. Required to specify which organization the invoice belongs to.",  # noqa: E501
    ],
    invoice_unique_identifier: Annotated[
        str, "Unique identifier for the invoice to be marked as void."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_invoice_void'."]:
    """Mark an invoice as void in Zoho Books.

    Use this tool to change the status of an invoice to void in Zoho Books. This process will unassociate any payments and credits, placing them under customer credits."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/status/void".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_unique_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_invoice_as_draft(
    context: ToolContext,
    organization_identifier: Annotated[str, "The unique ID of the organization in Zoho Books."],
    invoice_identifier: Annotated[str, "Unique identifier of the invoice to be marked as draft."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_invoice_draft'."]:
    """Mark a voided invoice as draft in Zoho Books.

    This tool is used to change the status of a voided invoice to draft in Zoho Books. It should be called when there is a need to edit or resubmit an invoice previously marked as void."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/status/draft".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_invoices_email(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The organization ID for which invoices will be emailed. Required for sending emails.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    comma_separated_invoice_ids: Annotated[
        str | None,
        "Comma separated list of invoice IDs to be emailed. Maximum 10 IDs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_invoices'."]:
    """Send up to 10 invoices by email to customers.

    This tool is used to email invoices to your customers, allowing you to send up to 10 invoices in a single request.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["SENDINVOICESEMAIL_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not comma_separated_invoice_ids:
        missing_params.append(("comma_separated_invoice_ids", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDINVOICESEMAIL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDINVOICESEMAIL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["SENDINVOICESEMAIL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "invoice_ids": comma_separated_invoice_ids,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_invoice_from_sales_order(
    context: ToolContext,
    sales_order_id: Annotated[
        str, "The unique identifier of the confirmed sales order to create an invoice for."
    ],
    organization_id: Annotated[
        str,
        "ID of the organization for which the invoice is being created. This must be a valid string ID.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_invoice_from_salesorder'."]:
    """Create an invoice from a confirmed sales order.

    Use this tool to instantly generate an invoice based on confirmed sales orders. Ideal for automating billing processes directly from sales orders."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/fromsalesorder".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        params=remove_none_values({
            "salesorder_id": sales_order_id,
            "organization_id": organization_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def associate_invoice_with_sales_order(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Required for linking invoices with sales orders.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'map_invoice_with_salesorder'."]:
    """Link existing invoices to sales orders for tracking.

    This tool is used to associate existing invoices with one or more sales orders, enabling better tracking and management of orders and billing.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "ASSOCIATEINVOICEWITHSALESORDER_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ASSOCIATEINVOICEWITHSALESORDER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ASSOCIATEINVOICEWITHSALESORDER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/mapwithorder".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ASSOCIATEINVOICEWITHSALESORDER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def submit_invoice_for_approval(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    invoice_unique_id: Annotated[
        str, "The unique identifier for the invoice to be submitted for approval."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'submit_invoice'."]:
    """Submit an invoice for approval in Zoho Books.

    This tool allows users to submit an invoice for approval using the Zoho Books API. It should be called when an invoice is ready to be reviewed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/submit".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_unique_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def approve_invoice(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization for which the invoice is to be approved."
    ],
    invoice_identifier: Annotated[str, "Unique identifier of the invoice to approve."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approve_invoice'."]:
    """Approve a specified invoice for processing.

    This tool approves a pending invoice given its ID, making it ready for processing. Call this tool when an invoice requires approval to proceed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/approve".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_invoice_email_content(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique identifier for the organization. Required to retrieve invoice email content for the specified organization.",  # noqa: E501
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice to retrieve its email content."
    ],
    email_template_id: Annotated[
        str | None,
        "Optional. Specify a template ID to get the email content based on a specific template. Defaults to customer-associated or default template if not provided.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_invoice_email'."]:
    """Retrieve the email content for a specific invoice.

    Use this tool to get the formatted email content of a particular invoice by specifying the invoice ID. Ideal for scenarios where you need to access or send invoice details via email."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "email_template_id": email_template_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_invoice_email(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization to which the invoice is linked. This ID is required to specify which organization's invoice is being emailed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_identifier: Annotated[
        str | None,
        "Unique string identifier for the specific invoice to be emailed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_email_attachments: Annotated[
        str | None,
        "A comma-separated list of file paths to attach to the email. Provide file paths if additional files need to be included with the invoice email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    send_customer_statement: Annotated[
        bool | None,
        "Set to 'True' to send the customer statement PDF with the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    send_invoice_attachment: Annotated[
        bool | None,
        "Set to true to attach the invoice with the email; false to exclude it.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_invoice'."]:
    """Email an invoice to a customer with optional content customization.

    This tool is used to email an invoice to a customer. It can be called when there's a need to send invoice details via email, optionally allowing custom content. If no custom content is provided, default mail content will be used.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["SENDINVOICEEMAIL_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not invoice_identifier:
        missing_params.append(("invoice_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["SENDINVOICEEMAIL_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["SENDINVOICEEMAIL_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["SENDINVOICEEMAIL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "send_customer_statement": send_customer_statement,
            "send_attachment": send_invoice_attachment,
            "attachments": invoice_email_attachments,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def remind_customer_invoice_payment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. It specifies which organization's invoice reminders to manage.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_identifier: Annotated[
        str | None,
        "Unique identifier of the invoice to send a payment reminder for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    email_attachments: Annotated[
        str | None,
        "Comma-separated list of file URLs to attach to the reminder email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    include_customer_statement_pdf: Annotated[
        bool | None,
        "Set to true to include a customer statement PDF with the email reminder.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'remind_customer_for_invoice_payment'."
]:
    """Remind customers of unpaid invoices by email.

    Use this tool to notify customers about unpaid invoices through email. It works for invoices that are open or overdue.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "REMINDCUSTOMERINVOICEPAYMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not invoice_identifier:
        missing_params.append(("invoice_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["REMINDCUSTOMERINVOICEPAYMENT_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["REMINDCUSTOMERINVOICEPAYMENT_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/paymentreminder".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["REMINDCUSTOMERINVOICEPAYMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "send_customer_statement": include_customer_statement_pdf,
            "attachments": email_attachments,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_payment_reminder_email_content(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID for the organization within Zoho Books for which the payment reminder email content is being fetched.",  # noqa: E501
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier for the specific invoice to fetch the reminder email content."
    ],
) -> Annotated[
    dict[str, Any],
    "Response from the API endpoint 'get_payment_reminder_mail_content_for_invoice'.",
]:
    """Fetch the email content of a payment reminder for an invoice.

    Use this tool to obtain the email content for a payment reminder associated with a specific invoice. Ideal for scenarios where you need to preview or edit reminder emails before sending."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/paymentreminder".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_invoice_reminders(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "Provide the ID of the organization for which the invoice reminders are to be sent."
    ],
    invoice_ids: Annotated[
        str,
        "List of invoice IDs to send reminders for. Only for open or overdue invoices, up to 10 at once.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_invoice_reminder'."]:
    """Send email reminders for unpaid invoices.

    Use this tool to remind customers about unpaid invoices by email. It sends reminder emails only for open or overdue invoices, with a maximum of 10 invoices at a time."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/paymentreminder".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "invoice_ids": invoice_ids,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def export_invoices_as_pdf(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization for which invoices are being exported."
    ],
    invoice_ids: Annotated[
        str, "Comma-separated list of invoice IDs to export as a PDF. Maximum of 25 IDs allowed."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_export_invoices_as_pdf'."]:
    """Export up to 25 invoices as a single PDF file.

    Use this tool to export multiple invoices into a single PDF document. Ideal for consolidating up to 25 invoices in one file."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/pdf".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "invoice_ids": invoice_ids,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def export_and_print_invoices(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to specify which organization's invoices to print.",  # noqa: E501
    ],
    invoice_identifiers: Annotated[
        str, "A comma-separated string of up to 25 invoice IDs to export and print as PDFs."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_print_invoices'."]:
    """Export and print multiple invoices as PDFs.

    Use this tool to export and print up to 25 invoices at a time in PDF format."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/print".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "invoice_ids": invoice_identifiers,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def disable_invoice_payment_reminder(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to specify which organization's invoice reminders are being disabled.",  # noqa: E501
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice to disable payment reminders."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'disable_invoice_payment_reminder'."
]:
    """Disable automated payment reminders for an invoice."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/paymentreminder/disable".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_invoice_reminder(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the invoice payment reminder is being activated."
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice for which payment reminders are to be activated."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'enable_invoice_payment_reminder'."]:
    """Enable automated payment reminders for invoices.

    Activate automatic reminders for invoice payments to ensure timely settlements."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/paymentreminder/enable".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def write_off_invoice_balance(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID of the organization in Zoho Books."],
    invoice_identifier: Annotated[str, "The unique identifier for the invoice to be written off."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'write_off_invoice'."]:
    """Write off the balance amount of an invoice in Zoho Books.

    Use this tool to write off the remaining balance of an invoice in Zoho Books when the amount is uncollectible."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/writeoff".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def cancel_write_off_invoice(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization in Zoho Books whose invoice write-off is to be canceled."
    ],
    invoice_unique_identifier: Annotated[
        str, "The unique identifier for the invoice whose write-off is to be canceled."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'cancel_write_off_invoice'."]:
    """Cancel the write-off amount of an invoice in Zoho Books.

    Use this tool to revert the write-off process of an invoice in Zoho Books. This action can be performed when an invoice's write-off needs to be canceled, typically to amend financial records or correct a mistake."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/writeoff/cancel".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_unique_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def modify_invoice_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique string ID of the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_identifier: Annotated[
        str | None,
        "Unique identifier of the invoice to update the billing address.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_invoice_billing_address'."]:
    """Update the billing address for a specific invoice.

    Use this tool to update the billing address for a particular invoice in Zoho Books. It modifies the address details for a specified invoice.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["MODIFYINVOICEADDRESS_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not invoice_identifier:
        missing_params.append(("invoice_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["MODIFYINVOICEADDRESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["MODIFYINVOICEADDRESS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/address/billing".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["MODIFYINVOICEADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_invoice_shipping_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization in Zoho Books. Required to specify which organization's invoice will be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_unique_identifier: Annotated[
        str | None,
        "Unique identifier of the invoice to update the shipping address.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_invoice_shipping_address'."]:
    """Update the shipping address of a specific invoice.

    Use this tool to update the shipping address associated with a specific invoice in Zoho Books. It is used when there's a need to modify the shipping information for an invoice after it has been created.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEINVOICESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not invoice_unique_identifier:
        missing_params.append(("invoice_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEINVOICESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEINVOICESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/address/shipping".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_unique_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEINVOICESHIPPINGADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_invoice_templates(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to fetch invoice templates for."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_invoice_templates'."]:
    """Fetch all invoice PDF templates from Zoho Books.

    This tool retrieves a list of all available invoice PDF templates in Zoho Books. Use this to access template options for invoices."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/templates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_invoice_template(
    context: ToolContext,
    organization_id: Annotated[
        str, "Provide the ID of the organization for which the invoice template is being updated."
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier for the invoice to update the PDF template."
    ],
    invoice_template_id: Annotated[
        str, "Unique identifier for the invoice template to be updated."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_invoice_template'."]:
    """Update the PDF template for a specific invoice.

    Use this tool to update the PDF template associated with a particular invoice. This is useful when you need to change the template style or format of an invoice after it has been issued."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/templates/{template_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_identifier,
            template_id=invoice_template_id,
        ),
        method="PUT",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_invoice_payments(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to retrieve invoice payments for."],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice to retrieve its payment details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_invoice_payments'."]:
    """Retrieve a list of payments for a specific invoice.

    Use this tool to get detailed information about payments made for a specific invoice by providing the invoice ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/payments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_invoice_credits_applied(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to retrieve credits applied to an invoice.",  # noqa: E501
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice for which credits are applied."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_invoice_credits_applied'."]:
    """Retrieve the credits applied to a specific invoice.

    Use this tool to obtain a list of all credits that have been applied to a particular invoice. It should be called when detailed invoice credit information is needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/creditsapplied".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def apply_credits_to_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization to identify where credits are applied.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_unique_identifier: Annotated[
        str | None,
        "Unique identifier of the invoice to which credits will be applied.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'apply_credits_to_invoice'."]:
    """Apply customer credits to an invoice.

    This tool applies customer credits, from credit notes or excess payments, to a specified invoice. It can apply multiple credits at once.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "APPLYCREDITSTOINVOICE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not invoice_unique_identifier:
        missing_params.append(("invoice_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["APPLYCREDITSTOINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["APPLYCREDITSTOINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/credits".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_unique_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["APPLYCREDITSTOINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_invoice_payment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books to which the payment belongs. This is required to identify the specific organization for deleting the invoice payment.",  # noqa: E501
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice to delete the payment from."
    ],
    invoice_payment_identifier: Annotated[
        str, "Unique identifier of the invoice payment to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_invoice_payment'."]:
    """Delete a payment made to an invoice in Zoho Books.

    Use this tool to remove a payment record from an invoice in Zoho Books when it was entered incorrectly or is no longer needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/payments/{invoice_payment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_identifier,
            invoice_payment_id=invoice_payment_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def remove_invoice_credit(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization from which the credit is being removed."
    ],
    invoice_identifier: Annotated[str, "Unique identifier of the invoice to remove a credit from."],
    credit_note_invoice_id: Annotated[
        str, "Unique identifier of the credit note invoice to be removed from the invoice."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_invoice_applied_credit'."]:
    """Remove a specific credit applied to an invoice.

    Use this tool to delete a particular credit that has been applied to an invoice when adjustments are needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/creditsapplied/{creditnotes_invoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_identifier,
            creditnotes_invoice_id=credit_note_invoice_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_invoice_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization to retrieve the invoice attachment for."
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice to fetch the attachment from."
    ],
    get_thumbnail: Annotated[
        bool | None, "Set to true to get the thumbnail of the invoice attachment."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_invoice_attachment'."]:
    """Fetch attachment file from a specified invoice.

    Call this tool to retrieve the file attached to a specific invoice using its ID. Useful for accessing documents associated with invoices."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id, "preview": get_thumbnail}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def attach_invoice_file(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "ID of the organization for which the invoice attachment is being added. This is required to identify the specific organization within Zoho Books.",  # noqa: E501
    ],
    invoice_identifier: Annotated[str, "Unique identifier for the invoice to attach the file to."],
    file_to_attach: Annotated[
        str | None, "The file to be attached. Allowed extensions: gif, png, jpeg, jpg, bmp, pdf."
    ] = None,
    send_attachment_in_email: Annotated[
        bool | None, "Set to True to send the attachment with the invoice when emailed."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_invoice_attachment'."]:
    """Attach a file to a specified invoice.

    Use this tool to attach a file to a specific invoice using its ID. It is useful for adding documents related to the invoice."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "can_send_in_mail": send_attachment_in_email,
            "attachment": file_to_attach,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def set_invoice_attachment_preference(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    invoice_identifier: Annotated[
        str, "Unique identifier for the specific invoice to update attachment preference."
    ],
    send_attachment_with_email: Annotated[
        bool, "Set to true to send the attachment with the invoice when emailed."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_attachment_preference'."]:
    """Set the email attachment preference for an invoice.

    This tool updates whether an attached file should be sent when emailing a specific invoice in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="PUT",
        params=remove_none_values({
            "organization_id": organization_id,
            "can_send_in_mail": send_attachment_with_email,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_invoice_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "ID of the organization for which the invoice attachment will be deleted. This is required to authenticate and identify the specific organization on Zoho Books.",  # noqa: E501
    ],
    invoice_identifier: Annotated[
        str,
        "Unique identifier of the invoice to delete the attachment from. Must match the invoice's ID in Zoho Books.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_invoice_attachment'."]:
    """Delete the file attached to an invoice.

    This tool should be called to delete an attachment from a specified invoice in Zoho Books. Useful for managing or cleaning up invoice files."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_invoice_document(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to retrieve the document from."],
    invoice_id: Annotated[
        str,
        "The unique identifier for the invoice to which the document is attached. Required to retrieve the document.",  # noqa: E501
    ],
    invoice_document_id: Annotated[
        str,
        "Unique identifier for the specific document attached to the invoice. Required to retrieve the exact document.",  # noqa: E501
    ],
    response_format: Annotated[
        str | None, "Specify the desired format for the response, such as json, pdf, or html."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_invoice_document_details'."]:
    """Retrieve a document attached to a specific invoice.

    Use this tool to access and download a document linked to an invoice by specifying the invoice and document IDs."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/documents/{document_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_id,
            document_id=invoice_document_id,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "responseformat": response_format,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_invoice_document(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. This is required to specify which organization's invoice document is to be deleted.",  # noqa: E501
    ],
    invoice_id: Annotated[
        str,
        "The unique identifier of the invoice from which the document will be deleted. This ID is required and must be a valid invoice in the system.",  # noqa: E501
    ],
    invoice_document_id: Annotated[
        str, "The unique ID of the document to be deleted from the invoice."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_invoice_document'."]:
    """Delete a document attached to an invoice.

    Permanently removes a document from an invoice in Zoho Books. This tool should be called when there is a need to delete a specific non-system-generated document associated with an invoice."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/documents/{document_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_id,
            document_id=invoice_document_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_invoice_expense_receipt(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization from which the expense receipt will be deleted."
    ],
    expense_identifier: Annotated[str, "Unique identifier of the expense to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_invoice_expense_receipt'."]:
    """Delete attached expense receipts from an invoice.

    Use this tool to delete expense receipts that are attached to an invoice, specifically those raised from an expense."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/expenses/{expense_id}/receipt".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), expense_id=expense_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_invoice_custom_fields(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization to which the invoice belongs. This is required to identify the correct organization context for the invoice update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_identifier: Annotated[
        str | None,
        "Unique identifier of the invoice to update custom fields.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_custom_fields_in_invoice'."]:
    """Update custom fields in an existing invoice.

    Use this tool to modify the value of custom fields in a specified invoice. It should be called when you need to change or update custom information for invoice records.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEINVOICECUSTOMFIELDS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not invoice_identifier:
        missing_params.append(("invoice_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEINVOICECUSTOMFIELDS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEINVOICECUSTOMFIELDS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoice/{invoice_id}/customfields".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEINVOICECUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_invoice_comments(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "ID of the organization for which the invoice comments and history are being retrieved. Must be a unique string identifier.",  # noqa: E501
    ],
    invoice_identifier: Annotated[
        str, "Unique identifier for the specific invoice to retrieve comments and history."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_invoice_comments'."]:
    """Get comments and history of an invoice.

    Use this tool to retrieve the full history and all comments associated with a specific invoice."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_invoice_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization for which the invoice belongs. It must be a valid and existing organization ID.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_identifier: Annotated[
        str | None,
        "Unique identifier of the invoice to add a comment to.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_invoice_comment'."]:
    """Add a comment to a specific invoice.

    Use this tool to add a comment to an invoice by specifying the invoice ID. Useful for internal notes or communication related to invoice handling.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDINVOICECOMMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not invoice_identifier:
        missing_params.append(("invoice_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDINVOICECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDINVOICECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), invoice_id=invoice_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDINVOICECOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_invoice_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "ID of the organization for which the invoice comment needs to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_unique_id: Annotated[
        str | None,
        "Unique identifier for the invoice to update its comment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    comment_id: Annotated[
        str | None,
        "Unique identifier of the comment to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_invoice_comment'."]:
    """Update an existing comment on an invoice.

    Use this tool to update a specific comment on an invoice by providing the invoice and comment IDs.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEINVOICECOMMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not invoice_unique_id:
        missing_params.append(("invoice_unique_id", "path"))
    if not comment_id:
        missing_params.append(("comment_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEINVOICECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEINVOICECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_unique_id,
            comment_id=comment_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEINVOICECOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_invoice_comment(
    context: ToolContext,
    organization_id: Annotated[str, "Identifier for the organization in Zoho Books."],
    invoice_identifier: Annotated[
        str, "Unique identifier of the invoice to delete the comment from."
    ],
    comment_unique_identifier: Annotated[
        str, "The unique identifier for the comment to be deleted from the invoice."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_invoice_comment'."]:
    """Delete a specific comment from an invoice.

    Use this tool to remove a comment from an invoice in Zoho Books. It is useful when a comment is no longer relevant or was added by mistake."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/invoices/{invoice_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            invoice_id=invoice_identifier,
            comment_id=comment_unique_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.ALL"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def generate_invoice_payment_link(
    context: ToolContext,
    organization_identifier: Annotated[str, "Specify the organization's unique ID."],
    invoice_transaction_id: Annotated[
        str, "The unique ID of the transaction or invoice for which the payment link is generated."
    ],
    transaction_type: Annotated[str, "Specifies the type of transaction, typically 'Invoice'."],
    link_type: Annotated[str, "Specifies whether the payment link is Private or Public."],
    payment_link_expiry_date: Annotated[
        str, "The date when the payment link should expire. Use format: yyyy-MM-dd."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'generate_invoice_payment_link'."]:
    """Generate a payment link for an invoice with expiry.

    Use this tool to generate a unique payment link for a specified invoice, including setting an expiry date for the link."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/share/paymentlink".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "transaction_id": invoice_transaction_id,
            "transaction_type": transaction_type,
            "link_type": link_type,
            "expiry_time": payment_link_expiry_date,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_new_zoho_item(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_item'."]:
    """Create a new item in Zoho Books inventory.

    Use this tool to create a new item in Zoho Books. This is useful for adding products or services to your Zoho inventory for tracking purposes.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATENEWZOHOITEM_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATENEWZOHOITEM_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATENEWZOHOITEM_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/items".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATENEWZOHOITEM_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_active_inventory_items(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to fetch items from."
    ],
    item_name_search: Annotated[
        str | None,
        "Search for items by name using prefixes 'name_startswith' or 'name_contains'. Maximum length is 100 characters.",  # noqa: E501
    ] = None,
    description_filter: Annotated[
        str | None,
        "Search items by description. Use keywords or phrases up to 100 characters. Prefix with 'description_startswith' or 'description_contains' for specific filtering.",  # noqa: E501
    ] = None,
    search_by_rate_criteria: Annotated[
        str | None, "Specify rate conditions to filter items. Use format like 'rate_less_than:100'."
    ] = None,
    search_by_tax_id: Annotated[
        str | None, "Search for items using the tax ID as a filter."
    ] = None,
    tax_name_filter: Annotated[str | None, "Filter items by their tax name."] = None,
    tax_exemption_identifier: Annotated[
        str | None, "ID for the tax exemption. Required if is_taxable is false."
    ] = None,
    associated_account_id: Annotated[
        str | None, "ID of the account to associate the item with."
    ] = None,
    filter_items_by_status: Annotated[
        str | None,
        "Filter items by status. Allowed values are 'Status.All', 'Status.Active', and 'Status.Inactive'.",  # noqa: E501
    ] = None,
    search_items_by_text: Annotated[
        str | None, "Search for items by name or description, up to 100 characters."
    ] = None,
    sort_items_by: Annotated[
        str | None,
        "Specify the attribute to sort items by. Allowed values: 'name', 'rate', 'tax_name'.",
    ] = None,
    sat_item_key_code: Annotated[
        str | None,
        "SAT Item key code used to filter items. Provide a valid string key code for lookup.",
    ] = None,
    sat_unit_code: Annotated[
        str | None,
        "SAT Unit code for specific inventory items. Used to search or filter items based on their unit code.",  # noqa: E501
    ] = None,
    page_number_to_fetch: Annotated[
        int | None,
        "The page number of active items to retrieve, with a default of 1 if unspecified.",
    ] = 1,
    records_per_page: Annotated[
        int | None, "Specify the number of records to fetch per page. Default is 200."
    ] = 200,
    is_item_taxable: Annotated[
        bool | None, "Boolean indicating if the item is taxable. True means the item is taxable."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_items'."]:
    """Retrieve a paginated list of all active inventory items.

    Use this tool to fetch a list of all active items from the inventory. It provides the data with pagination to manage large sets of items efficiently."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/items".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "name": item_name_search,
            "description": description_filter,
            "rate": search_by_rate_criteria,
            "tax_id": search_by_tax_id,
            "tax_name": tax_name_filter,
            "is_taxable": is_item_taxable,
            "tax_exemption_id": tax_exemption_identifier,
            "account_id": associated_account_id,
            "filter_by": filter_items_by_status,
            "search_text": search_items_by_text,
            "sort_column": sort_items_by,
            "sat_item_key_code": sat_item_key_code,
            "unitkey_code": sat_unit_code,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_item_via_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. This ID is used to specify which organization's data you are trying to access or modify.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "The API name of the unique custom field used for identifying the item.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "The unique value of the custom field used to identify or create an item in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_item_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new item if no item matches the unique custom field value.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_item_using_custom_field'."]:
    """Update or create an item using a unique custom field.

    Use this tool to update an existing item or create a new one based on a unique custom field value in Zoho Books. If the custom field value matches, the item is updated; otherwise, a new item is created if allowed.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEITEMVIACUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEITEMVIACUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEITEMVIACUSTOMFIELD_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/items".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEITEMVIACUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": create_item_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_zoho_item_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization in Zoho Books for which the item details are to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    item_identifier: Annotated[
        str | None,
        "Unique identifier of the item to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_item'."]:
    """Update the details of an item in Zoho Books.

    Use this tool to update item details in Zoho Books by providing the item ID and the new information to be updated.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEZOHOITEMDETAILS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not item_identifier:
        missing_params.append(("item_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEZOHOITEMDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEZOHOITEMDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/items/{item_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), item_id=item_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEZOHOITEMDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_item_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization whose item details are being retrieved."
    ],
    item_unique_identifier: Annotated[
        str, "Unique identifier for the item to retrieve details from Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_item'."]:
    """Retrieve details of a specific item in Zoho Books.

    Use this tool to get detailed information about a specific item in Zoho Books by providing the item ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/items/{item_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), item_id=item_unique_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_item_in_zoho_books(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization in Zoho Books from which you wish to delete the item."
    ],
    item_identifier: Annotated[str, "Unique identifier of the item to be deleted from Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_item'."]:
    """Delete an item from Zoho Books.

    This tool deletes an item from Zoho Books. It should be called to remove items that are no longer needed, provided they are not part of any transaction."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/items/{item_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), item_id=item_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_item_custom_fields(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization associated with the item. This is required to specify which organization's item custom fields should be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    item_identifier: Annotated[
        str | None,
        "Provide the unique identifier for the item to update its custom fields.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_custom_fields_in_item'."]:
    """Updates custom fields in an existing item.

    Use this tool to update the value of custom fields in existing items. Call it when you need to modify specific custom field information for an item record in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEITEMCUSTOMFIELDS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not item_identifier:
        missing_params.append(("item_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEITEMCUSTOMFIELDS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEITEMCUSTOMFIELDS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/item/{item_id}/customfields".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), item_id=item_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEITEMCUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_inactive_item(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for your organization in Zoho Books. Required to activate an item.",
    ],
    item_identifier: Annotated[str, "Unique identifier of the item to be activated in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_item_active'."]:
    """Activate an inactive item in Zoho Books.

    Use this tool to reactivate an item that has been previously marked as inactive in Zoho Books. It should be called when you need to make an item available again for transactions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/items/{item_id}/active".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), item_id=item_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_item_inactive(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. Required for specifying which organization's item to mark as inactive.",  # noqa: E501
    ],
    item_identifier: Annotated[str, "Unique identifier of the item to be marked inactive."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_item_inactive'."]:
    """Mark an item as inactive in Zoho Books.

    Use this tool to mark an active item as inactive in Zoho Books. Useful for managing inventory and item status."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/items/{item_id}/inactive".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), item_id=item_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_journal_entry(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_journal'."]:
    """Create a journal entry in Zoho Books.

    Use this tool to add a new journal entry in Zoho Books. It should be called when there's a need to record financial transactions manually.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEJOURNALENTRY_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEJOURNALENTRY_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEJOURNALENTRY_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/journals".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEJOURNALENTRY_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_journal_list(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to retrieve journals for."
    ],
    journal_entry_number: Annotated[
        str | None,
        "Search journals by journal entry number using exact match or variants like 'entry_number_startswith' and 'entry_number_contains'.",  # noqa: E501
    ] = None,
    search_by_reference_number: Annotated[
        str | None,
        "Search journals by reference number. Use 'startswith:' or 'contains:' for filtering options.",  # noqa: E501
    ] = None,
    journal_date_search: Annotated[
        str | None,
        "Specify date criteria to search journals. Use date_start, date_end, date_before, or date_after.",  # noqa: E501
    ] = None,
    search_journal_notes: Annotated[
        str | None,
        "Search journals by their associated notes. Options: 'startswith' or 'contains'.",
    ] = None,
    search_by_last_modified_time: Annotated[
        str | None,
        "Search for journals using the last modified time as a filter criterion. Provide a valid timestamp to filter entries updated after that time.",  # noqa: E501
    ] = None,
    journal_total_filter: Annotated[
        float | None,
        "Filter journals based on total amount using keys like total_less_than or total_greater_equals.",  # noqa: E501
    ] = None,
    search_by_customer_id: Annotated[
        int | None, "Use a specific Customer ID to search for journals in Zoho Books."
    ] = None,
    vendor_id: Annotated[
        int | None, "Specify the Vendor ID to search journals associated with that vendor."
    ] = None,
    filter_journals_by_date: Annotated[
        str | None,
        "Specify the time period to filter journals by date. Options: JournalDate.All, JournalDate.Today, JournalDate.ThisWeek, JournalDate.ThisMonth, JournalDate.ThisQuarter, JournalDate.ThisYear.",  # noqa: E501
    ] = None,
    sorting_column_for_journals: Annotated[
        str | None,
        "Specify the field to sort journals by. Options: 'journal_date', 'entry_number', 'reference_number', 'total'.",  # noqa: E501
    ] = None,
    page_number_to_fetch: Annotated[
        int | None, "Page number of the journal list to retrieve. Default value is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of journal records to be fetched per page. Default value is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_journals'."]:
    """Retrieve a list of accounting journals.

    Use this tool to get a list of accounting journals. It's useful for accessing journal entries in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/journals".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "entry_number": journal_entry_number,
            "reference_number": search_by_reference_number,
            "date": journal_date_search,
            "notes": search_journal_notes,
            "last_modified_time": search_by_last_modified_time,
            "total": journal_total_filter,
            "customer_id": search_by_customer_id,
            "vendor_id": vendor_id,
            "filter_by": filter_journals_by_date,
            "sort_column": sorting_column_for_journals,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_journal_in_zoho_books(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books. Required for identifying which organization's journal entry to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    journal_identifier: Annotated[
        str | None,
        "The unique identifier for the journal entry to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_journal'."]:
    """Updates a journal entry in Zoho Books with specified details.

    This tool updates an existing journal entry in Zoho Books using the provided details. It should be called when a user needs to modify a journal entry in their Zoho Books account.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEJOURNALINZOHOBOOKS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not journal_identifier:
        missing_params.append(("journal_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEJOURNALINZOHOBOOKS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEJOURNALINZOHOBOOKS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/journals/{journal_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), journal_id=journal_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEJOURNALINZOHOBOOKS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_journal_details(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization in Zoho Books required to retrieve journal details."
    ],
    journal_unique_id: Annotated[
        str, "The unique identifier for the journal to retrieve its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_journal'."]:
    """Retrieve the details of a specific journal entry in Zoho Books.

    This tool retrieves information about a specific journal entry from Zoho Books, using the journal ID. It should be called when details of a journal entry are needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/journals/{journal_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), journal_id=journal_unique_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_journal_entry(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization associated with the journal to be deleted."
    ],
    journal_entry_id: Annotated[str, "The unique identifier for the journal entry to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_journal'."]:
    """Delete a specific journal entry by ID.

    This tool deletes a journal entry in Zoho Books when provided with the specific journal ID. Use this to remove incorrect or unnecessary journal records."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/journals/{journal_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), journal_id=journal_entry_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def publish_draft_journal(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization in Zoho Books. Required to identify the organization where the journal resides.",  # noqa: E501
    ],
    journal_identifier: Annotated[
        str, "Unique identifier of the journal to be marked as published."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_journal_published'."]:
    """Mark a draft journal as published in Zoho Books.

    Use this tool to change the status of a draft journal entry to published within Zoho Books. This can be helpful when a draft is ready for final publication."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/journals/{journal_id}/status/publish".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), journal_id=journal_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def attach_file_to_journal(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization where the file will be attached. This is used to specify the target organization in Zoho Books.",  # noqa: E501
    ],
    journal_unique_identifier: Annotated[
        str,
        "Provide the unique identifier for the specific journal entry to which the file will be attached.",  # noqa: E501
    ],
    attachment_file_path: Annotated[
        str | None, "The path to the file that will be attached to the journal in Zoho Books."
    ] = None,
    document_to_attach: Annotated[
        str | None, "The document or file to be attached to the journal entry in Zoho Books."
    ] = None,
    total_number_of_files: Annotated[
        int | None,
        "Specify the total number of files to be attached to the journal. Ensure this matches the actual number of attachments.",  # noqa: E501
    ] = None,
    document_identifiers: Annotated[
        str | None,
        "A string of document IDs that need to be attached. These IDs should be associated with the documents intended for attachment.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_journal_attachment'."]:
    """Attach a file to a Zoho Books journal entry.

    This tool is used to attach a file to a specific journal entry in Zoho Books. Use it when you need to upload and associate documents or files with journal entries for record-keeping or documentation purposes."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/journals/{journal_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            journal_id=journal_unique_identifier,
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_id,
            "attachment": attachment_file_path,
            "doc": document_to_attach,
            "totalFiles": total_number_of_files,
            "document_ids": document_identifiers,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_journal_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization in Zoho Books where the comment is to be added.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    journal_unique_id: Annotated[
        str | None,
        "The unique identifier for the journal entry to which the comment will be added.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_journal_comment'."]:
    """Add a comment to a journal entry in Zoho Books.

    This tool adds a comment to a specified journal entry in Zoho Books. Call this tool when you need to annotate or provide additional information for a journal entry.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDJOURNALCOMMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not journal_unique_id:
        missing_params.append(("journal_unique_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDJOURNALCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDJOURNALCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/journals/{journal_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), journal_id=journal_unique_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDJOURNALCOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.accountants.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_journal_comment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to specify which organization's journal comment should be deleted.",  # noqa: E501
    ],
    journal_unique_id: Annotated[
        str, "Unique identifier of the journal for which the comment will be deleted."
    ],
    comment_id: Annotated[str, "Unique identifier of the comment to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_journal_comment'."]:
    """Delete a journal comment in Zoho Books.

    Use this tool to delete a specific comment from a journal entry in Zoho Books. It should be called when you need to remove unwanted or incorrect comments from a journal."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/journals/{journal_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            journal_id=journal_unique_id,
            comment_id=comment_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def enable_organization_locations(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier of the organization for which to enable location tracking."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'enable_locations'."]:
    """Enable locations for an organization in Zoho Books.

    Use this tool to enable location settings for an organization in Zoho Books, allowing tracking and management of different locations within the company's account."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/locations/enable".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_zoho_book_location(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the location is being created in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_location'."]:
    """Create a new location in Zoho Books.

    Use this tool to create a new business location within Zoho Books. It handles the setup of location data, allowing integration of new physical or digital locations into the Zoho Books system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATEZOHOBOOKLOCATION_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEZOHOBOOKLOCATION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEZOHOBOOKLOCATION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/locations".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEZOHOBOOKLOCATION_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_inventory_locations(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_locations'."]:
    """Retrieve all available locations from Zoho Inventory.

    Use this tool to get a comprehensive list of all the available locations in your Zoho Inventory. It should be called when you need to access inventory location details."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/locations".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_location_in_zoho_books(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization in Zoho Books. It is required to identify which organization's location is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    location_identifier: Annotated[
        str | None,
        "Unique identifier of the location to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_location'."]:
    """Update location details in Zoho Books.

    Use this tool to update location details in Zoho Books by specifying the location ID.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATELOCATIONINZOHOBOOKS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not location_identifier:
        missing_params.append(("location_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATELOCATIONINZOHOBOOKS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATELOCATIONINZOHOBOOKS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/locations/{location_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), location_id=location_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATELOCATIONINZOHOBOOKS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_location(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization to which the location belongs."
    ],
    location_id: Annotated[str, "The unique identifier of the location to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_location'."]:
    """Delete a location from the system.

    Use this tool to remove a location by specifying its unique identifier in the system."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/locations/{location_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), location_id=location_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_location(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to which the location belongs."],
    location_identifier: Annotated[
        str, "Unique identifier for the location to be marked as active."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_location_active'."]:
    """Marks a location as active.

    Use this tool to mark a specified location as active in the system. This is useful for enabling locations that were previously inactive."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/locations/{location_id}/active".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), location_id=location_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_location_inactive(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization in Zoho Books to mark the location as inactive."
    ],
    location_identifier: Annotated[
        str, "Unique identifier of the location to be marked as inactive in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_location_inactive'."]:
    """Marks a specific location as inactive in Zoho Books.

    Use this tool to set a location's status to inactive within Zoho Books. This is helpful for managing locations that are no longer in use."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/locations/{location_id}/inactive".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), location_id=location_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def set_primary_location(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization for which to set the primary location in Zoho Books."
    ],
    location_identifier: Annotated[
        str, "Unique identifier of the location to be marked as primary."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_location_primary'."]:
    """Marks a specified location as primary in Zoho Books.

    Use this tool to designate a specific location as the primary one in Zoho Books. This is useful for managing location preferences and ensuring the correct primary location is set."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/locations/{location_id}/markasprimary".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), location_id=location_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_opening_balance(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the opening balance is being created. This ID is required to specify the target organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_opening_balance'."]:
    """Creates an opening balance for accounts.

    Use this tool to create an opening balance with specified account information in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEOPENINGBALANCE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEOPENINGBALANCE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEOPENINGBALANCE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/openingbalances".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEOPENINGBALANCE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_opening_balance(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Required for updating the opening balance information.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_opening_balance'."]:
    """Update the existing opening balance information.

    This tool is used to update the existing opening balance information in Zoho Books. It should be called when there is a need to modify or correct the initial financial figures recorded in the system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEOPENINGBALANCE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEOPENINGBALANCE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEOPENINGBALANCE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/openingbalances".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEOPENINGBALANCE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_opening_balance(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization to retrieve the opening balance for."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_opening_balance'."]:
    """Retrieves the opening balance for accounts."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/openingbalances".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_opening_balance(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization whose opening balance is to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_opening_balance'."]:
    """Delete the entered opening balance in Zoho Books.

    Use this tool to remove the existing opening balance from Zoho Books settings when adjustments or corrections are needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/openingbalances".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_organization_in_zoho_books(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization to be created in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_organization'."]:
    """Create a new organization in Zoho Books.

    Use this tool to create a new organization in the Zoho Books platform. It is called when there's a need to add organizational details to Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATEORGANIZATIONINZOHOBOOKS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEORGANIZATIONINZOHOBOOKS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEORGANIZATIONINZOHOBOOKS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/organizations".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEORGANIZATIONINZOHOBOOKS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_organizations(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to list details for."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_organizations'."]:
    """Retrieve the list of organizations from Zoho Books.

    Use this tool to obtain a list of all organizations associated with the user's Zoho Books account. It is useful for managing and accessing organization-specific data or settings."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/organizations".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_organization_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique identifier of the organization to update in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    organization_identifier: Annotated[
        str | None,
        "The unique string identifier for the organization to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_organization'."]:
    """Update an organization's details in Zoho Books.

    This tool updates the details of an organization in Zoho Books. It should be called when you need to modify existing information about a specific organization.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEORGANIZATIONDETAILS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEORGANIZATIONDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEORGANIZATIONDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/organizations/{organization_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            organization_id=organization_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEORGANIZATIONDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_organization_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "Unique identifier for the organization in Zoho Books. Used to retrieve specific organization details.",  # noqa: E501
    ],
    org_id: Annotated[str, "Unique identifier for the specific organization."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_organization'."]:
    """Retrieve details of an organization from Zoho Books.

    Use this tool to get comprehensive details about a specific organization in Zoho Books by providing the organization ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/organizations/{organization_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), organization_id=org_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": org_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_project(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique identifier for the organization in Zoho Books. Required for project creation.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_project'."]:
    """Create a new project in Zoho Books.

    This tool facilitates the creation of a new project within Zoho Books. It should be called when there's a need to start a new project and integrate it into the Zoho Books system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEPROJECT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATEPROJECT_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATEPROJECT_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEPROJECT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_projects(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization for which to list projects."],
    filter_projects_by_status: Annotated[
        str | None, "Filter projects by status. Use Status.All, Status.Active, or Status.Inactive."
    ] = None,
    search_by_customer_id: Annotated[
        str | None, "Search projects using the customer's ID to filter results."
    ] = None,
    sort_projects_by: Annotated[
        str | None, "Sort projects by project name, customer name, rate, or created time."
    ] = None,
    page_number: Annotated[
        int | None, "Specify the page number to retrieve. Defaults to 1 if not specified."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Defaults to 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_projects'."]:
    """Retrieve a list of all projects with pagination.

    This tool is used to get a list of all projects from Zoho Books, with support for pagination. Call this tool to retrieve project data when managing tasks or resources."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "filter_by": filter_projects_by_status,
            "customer_id": search_by_customer_id,
            "sort_column": sort_projects_by,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_project_with_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "A string representing the organization's ID required to update or create a project using the custom field.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "The API name of the unique custom field used to identify the project.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "The unique value for the custom field used to identify or create a project.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_new_project_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new project if no existing project matches the unique custom field value.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_projects_using_custom_field'."
]:
    """Update or create projects using a unique custom field.

    This tool updates an existing project or creates a new one if no match is found, using a unique custom field value in Zoho Books. Utilize it when you need to modify projects with specific identifiers.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEPROJECTWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPROJECTWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPROJECTWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPROJECTWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": create_new_project_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_project_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization within Zoho Books, required to identify the organization whose project is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    project_unique_identifier: Annotated[
        str | None,
        "Unique identifier of the project to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_project'."]:
    """Update details of a project in Zoho Books.

    This tool is called to update the details of a specific project within Zoho Books. It should be used when you need to modify project information such as its name, status, or any other attributes.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEPROJECTDETAILS_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not project_unique_identifier:
        missing_params.append(("project_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPROJECTDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPROJECTDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/{project_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_unique_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPROJECTDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_project_details(
    context: ToolContext,
    organization_identifier: Annotated[str, "ID of the organization to retrieve project details."],
    project_unique_identifier: Annotated[
        str, "Unique identifier for the project to retrieve detailed information."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_project'."]:
    """Retrieve detailed information of a specific project by ID.

    Call this tool to fetch details about a specific project using its ID. Useful for obtaining comprehensive project information."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_unique_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_project(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. Required to identify the organization from which the project will be deleted.",  # noqa: E501
    ],
    project_id: Annotated[str, "Unique identifier of the project to be deleted in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_project'."]:
    """Deletes an existing project in Zoho Books.

    Call this tool to delete an existing project in Zoho Books using the project ID. It confirms the deletion of the specified project."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), project_id=project_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_project(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization in which the project is to be activated."
    ],
    project_identifier: Annotated[
        str, "Unique identifier for the project to activate in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_project_active'."]:
    """Activate a project in Zoho Books.

    This tool marks a specified project as active in Zoho Books. Call this tool when you need to change the status of a project to active to enable its functionalities or integrations."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/active".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), project_id=project_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def deactivate_project(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. Required for identifying the organization for the project update.",  # noqa: E501
    ],
    project_id: Annotated[str, "The unique identifier of the project to be marked as inactive."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_project_inactive'."]:
    """Deactivate a project in Zoho Books.

    Use this tool to mark a project as inactive in Zoho Books when it is no longer active or needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/inactive".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), project_id=project_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def clone_project(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    project_unique_identifier: Annotated[
        str | None,
        "Unique string identifier of the project to be cloned.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'clone_project'."]:
    """Clone an existing project in Zoho Books.

    Use this tool to create a copy of an existing project in Zoho Books. It's useful for duplicating project setups with similar parameters or settings.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CLONEPROJECT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not project_unique_identifier:
        missing_params.append(("project_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CLONEPROJECT_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CLONEPROJECT_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/{project_id}/clone".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_unique_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CLONEPROJECT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def assign_users_to_project(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique ID of the organization in Zoho Books for which users are being assigned to a project.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    project_identifier: Annotated[
        str | None,
        "The unique identifier for the project to which users will be assigned.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_project_user'."]:
    """Assign users to a specific project in Zoho Books.

    Use this tool to assign multiple users to a project in Zoho Books when managing project teams or updating project participation.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ASSIGNUSERSTOPROJECT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not project_identifier:
        missing_params.append(("project_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ASSIGNUSERSTOPROJECT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ASSIGNUSERSTOPROJECT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/{project_id}/users".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), project_id=project_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ASSIGNUSERSTOPROJECT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_project_users(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID representing the organization in Zoho Books. Required to access project users.",  # noqa: E501
    ],
    project_identifier: Annotated[
        str, "Unique identifier for the project to retrieve associated users."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_project_users'."]:
    """Get a list of users associated with a project.

    Use this tool to retrieve users linked to a specific project in Zoho Books. This is useful for managing project collaborations and assignments."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/users".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), project_id=project_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def invite_user_to_project(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization in Zoho Books where the project is located.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    project_unique_identifier: Annotated[
        str | None,
        "Unique identifier of the project in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'invite_project_user'."]:
    """Invite a user to a project in Zoho Books.

    Use this tool to send an invitation to a user to join a specific project in Zoho Books. This is helpful when you need to collaborate with others by adding them to project teams.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["INVITEUSERTOPROJECT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not project_unique_identifier:
        missing_params.append(("project_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["INVITEUSERTOPROJECT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["INVITEUSERTOPROJECT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/{project_id}/users/invite".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_unique_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["INVITEUSERTOPROJECT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_project_user_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Required to update the user's project details.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    project_identifier: Annotated[
        str | None,
        "Unique identifier for the project in Zoho Books. Required to specify which project's user details are being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    user_identifier: Annotated[
        str | None,
        "Unique identifier for the user to be updated within the project.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_project_user'."]:
    """Update user details in a specific project.

    Use this tool to update details of a user within a specified project in Zoho Books. It confirms the changes made to the user's information.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEPROJECTUSERDETAILS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not project_identifier:
        missing_params.append(("project_identifier", "path"))
    if not user_identifier:
        missing_params.append(("user_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPROJECTUSERDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPROJECTUSERDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/{project_id}/users/{user_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_identifier,
            user_id=user_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPROJECTUSERDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_project_user_details(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. This is required to fetch the user details associated with the specified project.",  # noqa: E501
    ],
    project_identifier: Annotated[
        str, "The unique identifier for the project in Zoho Books to fetch user details from."
    ],
    user_identifier: Annotated[
        str,
        "Unique identifier of the user within the project. Required to fetch user-specific details.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_project_user'."]:
    """Fetch details of a user within a project in Zoho Books.

    Use this tool to obtain information about a specific user's involvement in a particular project in Zoho Books. It is ideal for checking user roles or assignments within a project."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/users/{user_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_identifier,
            user_id=user_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def remove_user_from_project(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization within Zoho Books."
    ],
    project_identifier: Annotated[
        str, "The unique identifier for the project from which the user will be removed."
    ],
    user_identifier: Annotated[
        str, "Unique identifier of the user to be removed from the project."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_project_user'."]:
    """Remove a user from a specific project in Zoho Books.

    This tool is used to remove a user from a project within Zoho Books. Call this tool when you need to manage project participants by deleting a user's access to a particular project."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/users/{user_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_identifier,
            user_id=user_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def post_project_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique identifier of the organization for which the comment is being posted.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    project_unique_identifier: Annotated[
        str | None,
        "Unique identifier for the project in Zoho Books. Required to specify the target project for adding a comment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_project_comment'."]:
    """Post a comment to a specified project.

    This tool posts a comment to a specific project in Zoho Books. Use it when you need to add a comment or note to a project for tracking or communication purposes.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["POSTPROJECTCOMMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not project_unique_identifier:
        missing_params.append(("project_unique_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["POSTPROJECTCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["POSTPROJECTCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/{project_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_unique_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["POSTPROJECTCOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_project_comments(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization to fetch project comments for."
    ],
    project_identifier: Annotated[str, "Unique identifier of the project to fetch comments for."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_project_comments'."]:
    """Retrieve comments for a specified project.

    Use this tool to get detailed comments for a specified project by providing the project ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), project_id=project_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_project_comment(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the comment is to be deleted."
    ],
    project_identifier: Annotated[
        str, "Unique identifier of the project to delete the comment from."
    ],
    comment_unique_identifier: Annotated[str, "Unique identifier for the comment to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_project_comment'."]:
    """Delete a specific comment from a project.

    Use this tool to delete a comment from a specified project. Useful when you need to remove unwanted or outdated comments from project discussions."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_identifier,
            comment_id=comment_unique_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_project_invoices(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. This is required to access the specific organization's project invoices.",  # noqa: E501
    ],
    project_unique_identifier: Annotated[
        str, "Unique identifier for the specific project to retrieve invoices for."
    ],
    sort_invoices_by: Annotated[
        str | None,
        "Specify the column to sort invoices by. Options are: 'invoice_number', 'date', 'total', 'balance', 'created_time'.",  # noqa: E501
    ] = None,
    page_number_to_fetch: Annotated[
        int | None, "Specify the page number to retrieve from the list of invoices. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "The number of invoice records to fetch per page. Defaults to 200 if not specified.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_project_invoices'."]:
    """Retrieve invoices for a specific project in Zoho Books.

    Call this endpoint to obtain a list of all invoices associated with a particular project within Zoho Books. This tool is useful when you need to review or manage billing details for project-based work."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/invoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_unique_identifier,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "sort_column": sort_invoices_by,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_vendor_purchase_order(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books for which the purchase order is being created. Required to specify the target organization.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    file_attachment: Annotated[
        str | None,
        "File path or URL to attach. Allowed extensions: gif, png, jpeg, jpg, bmp, pdf, xls, xlsx, doc, docx.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to disable automatic purchase order number generation, requiring a manual number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_purchase_order'."]:
    """Generate a purchase order for a vendor.

    Use this tool to create a purchase order for a vendor in Zoho Books. It facilitates procurement by initiating an order with specified vendor details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATEVENDORPURCHASEORDER_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEVENDORPURCHASEORDER_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEVENDORPURCHASEORDER_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/purchaseorders".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEVENDORPURCHASEORDER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "attachment": file_attachment,
            "ignore_auto_number_generation": ignore_auto_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_purchase_orders(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID of the organization to filter purchase orders."],
    search_by_purchaseorder_number: Annotated[
        str | None,
        "Search purchase order by number. Supports exact, starts with, and contains variants.",
    ] = None,
    reference_number_search: Annotated[
        str | None,
        "Search for a purchase order using the exact or partial reference number. Supports 'startswith' and 'contains' methods.",  # noqa: E501
    ] = None,
    creation_date: Annotated[
        str | None, "Creation date for purchase order search in YYYY-MM-DD format."
    ] = None,
    purchase_order_status: Annotated[
        str | None, "Filter purchase orders by status. Options: draft, open, billed, cancelled."
    ] = None,
    search_by_item_description: Annotated[
        str | None,
        "Search purchase orders by item description. Use partial matches or specific description. Includes variants like 'startswith' and 'contains'.",  # noqa: E501
    ] = None,
    vendor_name: Annotated[
        str | None,
        "Search purchase orders by vendor name with optional 'startswith' or 'contains' variants.",
    ] = None,
    total_amount_filter: Annotated[
        float | None,
        "Filter purchase orders by total amount. Use options like 'start', 'end', 'less_than', 'less_equals', 'greater_than', 'greater_equals' to specify the range or comparison.",  # noqa: E501
    ] = None,
    vendor_identifier: Annotated[
        str | None,
        "Specify the unique ID of the vendor to filter purchase orders. Useful for grouping POs by a specific vendor.",  # noqa: E501
    ] = None,
    search_by_last_modified_time: Annotated[
        str | None,
        "ISO 8601 format (YYYY-MM-DDTHH:MM:SS±HH:MM) to filter POs by last modified time. For finding recently updated POs.",  # noqa: E501
    ] = None,
    search_by_item_id: Annotated[
        str | None,
        "Search purchase orders using the unique item ID to find POs containing a specific item.",
    ] = None,
    status_filter: Annotated[
        str | None,
        "Filter purchase orders by status. Use 'Status.All', 'Status.Draft', 'Status.Open', 'Status.Billed', or 'Status.Cancelled'.",  # noqa: E501
    ] = None,
    search_purchase_order_text: Annotated[
        str | None,
        "Search for purchase orders by number, reference, or vendor name. Allows general searching across multiple fields for quick lookup.",  # noqa: E501
    ] = None,
    sort_by_column: Annotated[
        str | None,
        "Column to sort purchase orders by. Options: vendor_name, purchaseorder_number, date, delivery_date, total, created_time.",  # noqa: E501
    ] = None,
    search_by_custom_field: Annotated[
        str | None,
        "Search purchase orders using custom field criteria. Supports 'startswith' and 'contains' variants.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None, "Specify the page number to fetch, with a default value of 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Specifies the number of purchase orders to retrieve per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_purchase_orders'."]:
    """Retrieve a list of all purchase orders."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "purchaseorder_number": search_by_purchaseorder_number,
            "reference_number": reference_number_search,
            "date": creation_date,
            "status": purchase_order_status,
            "item_description": search_by_item_description,
            "vendor_name": vendor_name,
            "total": total_amount_filter,
            "vendor_id": vendor_identifier,
            "last_modified_time": search_by_last_modified_time,
            "item_id": search_by_item_id,
            "filter_by": status_filter,
            "search_text": search_purchase_order_text,
            "sort_column": sort_by_column,
            "custom_field": search_by_custom_field,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_purchase_order_by_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. This is required to specify which organization's purchase order needs to be updated or created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "The API name of the unique custom field used to update or identify the purchase order.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "Unique value for the custom field to retrieve and update the purchase order. This should match the specific custom field value used to identify the order.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_new_order_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new purchase order if no existing order matches the unique custom field value.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_purchase_order_using_custom_field'."
]:
    """Update or create a purchase order via custom field value.

    Use this tool to update an existing purchase order or create a new one if needed, based on a unique custom field value. It should be called when you need to modify a purchase order by using a custom field's unique identifier.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEPURCHASEORDERBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/purchaseorders".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERBYCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": create_new_order_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_purchase_order(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization for which the purchase order is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    purchase_order_identifier: Annotated[
        str | None,
        "Unique identifier for the specific purchase order to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    attachment_file_path: Annotated[
        str | None,
        "File path of the attachment with extensions: gif, png, jpeg, jpg, bmp, pdf, xls, xlsx, doc, docx.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "If true, ignore automatic purchase order number generation and manually specify the order number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_purchase_order'."]:
    """Update an existing purchase order in Zoho Books.

    Use this tool to update details of a specific purchase order in Zoho Books. It should be called when changes to a purchase order need to be made.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDER_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not purchase_order_identifier:
        missing_params.append(("purchase_order_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDER_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDER_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchase_order_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchase_order_id=purchase_order_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "attachment": attachment_file_path,
            "ignore_auto_number_generation": ignore_auto_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_purchase_order_details(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "Unique ID of the organization to retrieve purchase order details."
    ],
    purchase_order_id: Annotated[
        str, "Provide the unique identifier of the purchase order to retrieve its details."
    ],
    response_format: Annotated[
        str | None,
        "Specifies the format of the purchase order details. Options: json, pdf, html. Default is json.",  # noqa: E501
    ] = None,
    print_pdf: Annotated[
        bool | None, "Set to True to print the exported PDF of the purchase order."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_purchase_order'."]:
    """Retrieve the details of a purchase order.

    Use this tool to get detailed information about a specific purchase order by providing the purchase order ID. Ideal for retrieving purchase order data for review or processing."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchase_order_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchase_order_id=purchase_order_id,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "print": print_pdf,
            "accept": response_format,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_purchase_order(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization within Zoho Books. Required to specify which organization's purchase order is to be deleted.",  # noqa: E501
    ],
    purchase_order_identifier: Annotated[
        str, "Unique identifier for the purchase order to delete."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_purchase_order'."]:
    """Delete an existing purchase order in Zoho Books.

    Use this tool to remove a purchase order by providing its ID. The endpoint confirms the deletion once the operation is successful."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchase_order_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchase_order_id=purchase_order_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_custom_fields_purchase_order(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization associated with the purchase order.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    purchase_order_id: Annotated[
        str | None,
        "A unique identifier for the purchase order to update custom fields.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_custom_fields_in_purchase_order'."
]:
    """Update custom field values in purchase orders.

    This tool updates the values of custom fields within existing purchase orders in Zoho Books. It should be called when there is a need to modify or update custom information in a specific purchase order.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATECUSTOMFIELDSPURCHASEORDER_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not purchase_order_id:
        missing_params.append(("purchase_order_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMFIELDSPURCHASEORDER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATECUSTOMFIELDSPURCHASEORDER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/purchaseorder/{purchaseorder_id}/customfields".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATECUSTOMFIELDSPURCHASEORDER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def open_purchase_order(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books whose purchase order status needs to be changed.",  # noqa: E501
    ],
    purchase_order_identifier: Annotated[
        str, "Unique identifier for the purchase order to be marked as open."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_purchase_order_open'."]:
    """Mark a draft purchase order as open.

    Use this tool to change the status of a draft purchase order to 'open' in Zoho Books, making it active and processable. Call this when you need to activate draft purchase orders."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/status/open".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_purchase_order_billed(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization in Zoho Books. Required to perform actions within the specified organization.",  # noqa: E501
    ],
    purchase_order_id: Annotated[
        str, "Unique identifier of the purchase order to be marked as billed."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_purchase_order_billed'."]:
    """Mark a purchase order as billed in Zoho Books.

    Use this tool to update the status of a purchase order to 'billed' in Zoho Books. This is useful when a purchase order has been fulfilled and needs to be recorded as billed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/status/billed".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def cancel_purchase_order(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The identifier for the organization in Zoho Books. This ID is required to specify which organization's purchase order should be cancelled.",  # noqa: E501
    ],
    purchase_order_id: Annotated[
        str, "The unique identifier of the purchase order to be cancelled."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_purchase_order_cancelled'."]:
    """Cancel a specific purchase order in Zoho Books.

    Use this tool to mark a purchase order as cancelled within the Zoho Books platform. This is useful when an order needs to be invalidated or stopped."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/status/cancelled".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def submit_purchase_order(
    context: ToolContext,
    organization_identifier: Annotated[str, "The unique ID of the organization within Zoho Books."],
    purchase_order_id: Annotated[
        str, "Unique identifier of the purchase order to be submitted for approval."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'submit_purchase_order'."]:
    """Submit a purchase order for approval.

    Use this tool to submit a specific purchase order for approval in the Zoho Books system."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/submit".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def approve_purchase_order(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization for which to approve the purchase order. This should be a unique string identifier provided by Zoho Books.",  # noqa: E501
    ],
    purchase_order_identifier: Annotated[
        str, "The unique identifier for the purchase order to be approved."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approve_purchase_order'."]:
    """Approve a purchase order.

    This tool approves a specified purchase order in Zoho Books. It should be called when a purchase order needs to be authorized for further processing."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/approve".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_purchase_order_email(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique identifier for the organization. Required to specify which organization the purchase order belongs to.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    purchase_order_id: Annotated[
        str | None,
        "Unique identifier of the purchase order to be emailed to the vendor.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    email_attachments: Annotated[
        str | None,
        "A comma-separated list of file paths or URLs to attach to the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    attachment_file_name: Annotated[
        str | None,
        "The name of the file to attach to the email for the purchase order.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    send_purchase_order_attachment: Annotated[
        bool | None,
        "Set to true to include the purchase order as an attachment with the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_purchase_order'."]:
    """Send a purchase order email to the vendor.

    Use this tool to email a purchase order to the vendor. If no custom content is provided, the email will be sent with default content.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "SENDPURCHASEORDEREMAIL_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not purchase_order_id:
        missing_params.append(("purchase_order_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDPURCHASEORDEREMAIL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDPURCHASEORDEREMAIL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["SENDPURCHASEORDEREMAIL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_identifier,
            "attachments": email_attachments,
            "send_attachment": send_purchase_order_attachment,
            "file_name": attachment_file_name,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_purchase_order_email_content(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization to retrieve the purchase order email content for.",  # noqa: E501
    ],
    purchase_order_id: Annotated[
        str, "Unique identifier of the purchase order to retrieve its email content."
    ],
    email_template_id: Annotated[
        str | None,
        "Get the email content based on a specific email template. Defaults to customer-associated or default template if not provided.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_purchase_order_email'."]:
    """Retrieves the email content of a purchase order.

    Use this tool to get the email content related to a specific purchase order by providing its ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "email_template_id": email_template_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_purchase_order_billing_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization to update the billing address in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    purchase_order_identifier: Annotated[
        str | None,
        "Unique identifier for the specific purchase order to update the billing address.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_purchase_order_billing_address'."
]:
    """Update the billing address for a specific purchase order.

    Use this tool to modify the billing address of a single purchase order in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEPURCHASEORDERBILLINGADDRESS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not purchase_order_identifier:
        missing_params.append(("purchase_order_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/address/billing".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_purchase_order_templates(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier of the organization for retrieving purchase order templates."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_purchase_order_templates'."]:
    """Retrieve all purchase order PDF templates from Zoho Books.

    This tool is used to obtain a list of all available purchase order PDF templates in Zoho Books. It should be called when there's a need to view or select from existing purchase order templates."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/templates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_purchase_order_attachment(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID representing the organization. Required to specify which organization's purchase order to access.",  # noqa: E501
    ],
    purchase_order_id: Annotated[
        str, "The unique identifier of the purchase order to retrieve the attachment for."
    ],
    get_thumbnail: Annotated[
        bool | None,
        "Set to true to get the thumbnail of the attachment, or false to retrieve the full file.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_purchase_order_attachment'."]:
    """Retrieve the file attached to a specific purchase order.

    Use this tool to obtain the file attachment from a specific purchase order by providing the purchase order ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "preview": get_thumbnail,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def attach_file_to_purchase_order(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    purchase_order_id: Annotated[
        str, "The unique identifier of the purchase order to which the file will be attached."
    ],
    file_attachment: Annotated[
        str | None,
        "The file to attach to the purchase order. Must be one of the following formats: gif, png, jpeg, jpg, bmp, pdf, xls, xlsx, doc, or docx.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_purchase_order_attachment'."]:
    """Attach a file to a specified purchase order.

    Use this tool to attach a file to a specified purchase order in Zoho Books. Useful for adding supplementary documents or files to your existing purchase orders."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_id,
            "attachment": file_attachment,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_purchase_order_email_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization for which the purchase order email attachment preference is being updated.",  # noqa: E501
    ],
    purchase_order_id: Annotated[
        str,
        "Unique identifier of the purchase order to update the email attachment preference for.",
    ],
    include_attachment_with_email: Annotated[
        bool,
        "Boolean to determine if the attachment should be sent with the purchase order email. Set to true to include the attachment, or false to exclude it.",  # noqa: E501
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_purchase_order_attachment'."
]:
    """Update email attachment preference for a purchase order.

    This tool updates the preference to include or exclude the attached file when emailing a specific purchase order. It should be called when you need to change whether the attachment is sent via email for a particular purchase order."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="PUT",
        params=remove_none_values({
            "organization_id": organization_id,
            "can_send_in_mail": include_attachment_with_email,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_purchase_order_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization in Zoho Books. This is required to specify which organization's records to access or modify.",  # noqa: E501
    ],
    purchase_order_id: Annotated[
        str, "Unique identifier of the purchase order to delete the attachment from."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'delete_purchase_order_attachment'."
]:
    """Deletes the attachment from a purchase order.

    This tool is used to delete the file attached to a specified purchase order in Zoho Books, identified by the purchase order ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_purchase_order_comments(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization to retrieve purchase order comments for. Required to specify which organization's data to access.",  # noqa: E501
    ],
    purchase_order_id: Annotated[
        str, "Unique identifier for the specific purchase order to retrieve comments and history."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_purchase_order_comments'."]:
    """Retrieve comments and history of a purchase order.

    Fetches the complete history and comments of a specified purchase order, useful for tracking and reviewing order details."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_purchase_order_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. This ID is required to specify which organization's purchase order is being commented on.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    purchase_order_identifier: Annotated[
        str | None,
        "Unique identifier for the purchase order in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_purchase_order_comment'."]:
    """Add a comment to a purchase order in Zoho Books.



    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "ADDPURCHASEORDERCOMMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not purchase_order_identifier:
        missing_params.append(("purchase_order_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDPURCHASEORDERCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDPURCHASEORDERCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDPURCHASEORDERCOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_purchase_order_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID representing the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    purchase_order_id: Annotated[
        str | None,
        "The unique identifier for the purchase order to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    comment_identifier: Annotated[
        str | None,
        "Unique identifier of the comment to be updated in the purchase order.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_purchase_order_comment'."]:
    """Update an existing comment on a purchase order.

    Use this tool to update a specific comment on a purchase order in Zoho Books. It should be called when you need to modify or correct information in a comment related to a purchase order.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEPURCHASEORDERCOMMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not purchase_order_id:
        missing_params.append(("purchase_order_id", "path"))
    if not comment_identifier:
        missing_params.append(("comment_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
            comment_id=comment_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPURCHASEORDERCOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_purchase_order_comment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. Provide this to specify which organization's purchase order comment you wish to delete.",  # noqa: E501
    ],
    purchase_order_id: Annotated[
        str, "Unique identifier of the purchase order to delete the comment from."
    ],
    comment_unique_identifier: Annotated[
        str,
        "Unique identifier of the comment to be deleted. Required to specify which comment to remove from a purchase order.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_purchase_order_comment'."]:
    """Delete a comment from a purchase order.

    Use this tool to remove a specific comment associated with a purchase order by providing the purchase order ID and the comment ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
            comment_id=comment_unique_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.purchaseorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def reject_purchase_order(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "A unique identifier for the organization in Zoho Books."
    ],
    purchase_order_id: Annotated[str, "The ID of the purchase order to be rejected in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'reject_purchase_orders'."]:
    """Reject a specific purchase order in Zoho Books.

    This tool should be called when you need to reject a purchase order in the Zoho Books system. It confirms the rejection of the specified purchase order."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/purchaseorders/{purchaseorder_id}/reject".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            purchaseorder_id=purchase_order_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_recurring_bill(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization in Zoho Books for which the recurring bill will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_recurring_bill'."]:
    """Create a recurring bill in Zoho Books.

    This tool creates a recurring bill in Zoho Books. Use it to automate periodic billing tasks and ensure payments are scheduled consistently.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATERECURRINGBILL_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATERECURRINGBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATERECURRINGBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringbills".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATERECURRINGBILL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_recurring_bill_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization associated with the recurring bill.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_field_unique_identifier_key: Annotated[
        str | None,
        "The API name of the unique custom field used to identify the recurring bill.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "Unique value of the custom field used to identify the recurring bill.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    allow_creation_if_missing: Annotated[
        bool | None,
        "Set to true to create a new recurring bill if the unique custom field value is not found.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_recurring_bill_using_custom_field'."
]:
    """Update or create a recurring bill using a unique custom field.

    This tool updates a recurring bill by identifying it through a custom field with a unique value. If the unique value does not match any existing recurring bills and the X-Upsert header is true, a new bill will be created if the necessary details are provided.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATERECURRINGBILLCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGBILLCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGBILLCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringbills".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATERECURRINGBILLCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": custom_field_unique_identifier_key,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": allow_creation_if_missing,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_recurring_bill(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. This ID is required to specify the organization whose recurring bill is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    recurring_bill_identifier: Annotated[
        str | None,
        "Unique identifier for the recurring bill to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_recurring_bill'."]:
    """Update details of a recurring bill in Zoho Books.

    Use this tool to update a recurring bill in Zoho Books. Modify its details including line items by removing them from the list if needed.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATERECURRINGBILL_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not recurring_bill_identifier:
        missing_params.append(("recurring_bill_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringbills/{recurring_bill_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_bill_id=recurring_bill_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATERECURRINGBILL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_recurring_bill_details(
    context: ToolContext,
    organization_identifier: Annotated[str, "The unique ID of the organization in Zoho Books."],
    recurring_bill_unique_id: Annotated[
        str, "Unique identifier for the recurring bill in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_recurring_bill'."]:
    """Retrieve details of a recurring bill from Zoho Books.

    This tool retrieves comprehensive details of a specified recurring bill using its ID. It should be called when detailed information about a recurring bill is required from Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurring_bills/{recurring_bill_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_bill_id=recurring_bill_unique_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_recurring_bill(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "ID of the organization in Zoho Books to delete a recurring bill from."
    ],
    recurring_bill_identifier: Annotated[
        str, "Unique identifier of the recurring bill to be deleted in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_recurring_bill'."]:
    """Delete an existing recurring bill in Zoho Books.

    Use this tool to delete a recurring bill identified by its ID in Zoho Books. This is useful when managing financial records and removing bills that are no longer needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurring_bills/{recurring_bill_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_bill_id=recurring_bill_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def stop_recurring_bill(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID representing the organization in Zoho Books."],
    recurring_bill_identifier: Annotated[
        str, "Unique identifier for the recurring bill to be stopped."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'stop_recurring_bill'."]:
    """Stop an active recurring bill in Zoho Books.

    Use this tool to stop an active recurring bill in Zoho Books when you need to discontinue ongoing payments. It provides confirmation upon successful stoppage."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringbills/{recurring_bill_id}/status/stop".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_bill_id=recurring_bill_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def resume_recurring_bill(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID for the organization in Zoho Books."],
    recurring_bill_identifier: Annotated[
        str, "Provide the unique identifier of the recurring bill to resume it in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'resume_recurring_bill'."]:
    """Resume a stopped recurring bill in Zoho Books.

    Use this tool to restart a suspended recurring bill in Zoho Books, ensuring payments continue as scheduled."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringbills/{recurring_bill_id}/status/resume".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_bill_id=recurring_bill_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.bills.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_recurring_bill_history(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which you want to get the recurring bill history."
    ],
    recurring_bill_identifier: Annotated[
        str,
        "Unique identifier for the specific recurring bill. Required to fetch its history and comments.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_recurring_bill_history'."]:
    """Get history and comments of a recurring bill.

    Use this tool to fetch detailed history and comments related to a specific recurring bill by providing the recurring bill ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringbills/{recurring_bill_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_bill_id=recurring_bill_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_recurring_expense(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. This ID is necessary to specify which organization's records to create the recurring expense under.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_recurring_expense'."]:
    """Create a recurring expense in Zoho Books.

    Use this tool to create a recurring expense in Zoho Books. It allows you to automate expense tracking by setting expenses to recur at specified intervals.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATERECURRINGEXPENSE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATERECURRINGEXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATERECURRINGEXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringexpenses".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATERECURRINGEXPENSE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_recurring_expenses(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier of the organization whose recurring expenses need to be listed."
    ],
    recurring_expense_name_filter: Annotated[
        str | None,
        "Filter recurring expenses by name using either 'startswith' or 'contains'. Maximum length is 100 characters.",  # noqa: E501
    ] = None,
    search_by_last_created_date: Annotated[
        str | None,
        "Filter recurring expenses based on last generated expense date. Use formats: <code>last_created_date_start</code>, <code>last_created_date_end</code>, <code>last_created_date_before</code>, <code>last_created_date_after</code> with date [yyyy-mm-dd].",  # noqa: E501
    ] = None,
    filter_by_next_expense_date: Annotated[
        str | None,
        "Filter recurring expenses by dates related to the next expected expense. Options include 'next_expense_date_start', 'next_expense_date_end', 'next_expense_date_before', and 'next_expense_date_after'. Format is 'yyyy-mm-dd'.",  # noqa: E501
    ] = None,
    expense_status: Annotated[
        str | None,
        "Specify the status of expenses to search for. Allowed values are 'active', 'stopped', and 'expired'.",  # noqa: E501
    ] = None,
    account_id_for_expense: Annotated[
        str | None,
        "Specify the unique identifier for the expense account to filter expenses associated with it.",  # noqa: E501
    ] = None,
    filter_by_account_name: Annotated[
        str | None,
        "Search expenses by account name with options for exact match or partial match using 'startswith' and 'contains'. Max-length is 100 characters.",  # noqa: E501
    ] = None,
    amount_filter: Annotated[
        float | None,
        "Specify a filter for expense amounts, such as 'amount_less_than', 'amount_less_equals', 'amount_greater_than', or 'amount_greater_than'.",  # noqa: E501
    ] = None,
    search_by_customer_name: Annotated[
        str | None,
        "Search recurring expenses by customer name. Use variants 'customer_name_startswith' or 'customer_name_contains'. Max 100 characters.",  # noqa: E501
    ] = None,
    search_by_customer_id: Annotated[
        str | None, "Specify the customer ID to search expenses associated with that customer."
    ] = None,
    paid_through_account_id: Annotated[
        str | None, "ID of the account through which the expense was paid. Used to filter expenses."
    ] = None,
    expense_status_filter: Annotated[
        str | None,
        "Filter recurring expenses by their status. Use 'Status.All', 'Status.Active', 'Status.Expired', or 'Status.Stopped'.",  # noqa: E501
    ] = None,
    search_expenses_by_text: Annotated[
        str | None,
        "Specify text to search expenses by account name, description, customer name, or vendor name. Maximum length is 100 characters.",  # noqa: E501
    ] = None,
    sort_expenses_by_column: Annotated[
        str | None,
        "Specify the column to sort expenses by. Allowed values: next_expense_date, account_name, total, last_created_date, recurrence_name, customer_name, created_time.",  # noqa: E501
    ] = None,
    page_number_to_fetch: Annotated[
        int | None, "The page number of records to retrieve, starting from 1. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Specify how many records to retrieve per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_recurring_expenses'."]:
    """Retrieve all recurring expenses from your records.

    Use this tool to fetch a list of all recurring expenses. Ideal for situations where you need to review or manage periodic expenses systematically."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringexpenses".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "recurrence_name": recurring_expense_name_filter,
            "last_created_date": search_by_last_created_date,
            "next_expense_date": filter_by_next_expense_date,
            "status": expense_status,
            "account_id": account_id_for_expense,
            "account_name": filter_by_account_name,
            "amount": amount_filter,
            "customer_name": search_by_customer_name,
            "customer_id": search_by_customer_id,
            "paid_through_account_id": paid_through_account_id,
            "filter_by": expense_status_filter,
            "search_text": search_expenses_by_text,
            "sort_column": sort_expenses_by_column,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_recurring_expense(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique identifier for the organization whose recurring expense is to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "Unique CustomField API Name to identify the recurring expense.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "Unique value of the CustomField used to identify the recurring expense.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_new_recurring_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new recurring expense if the unique custom field value is not found.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_recurring_expense_using_custom_field'."
]:
    """Update or create a recurring expense using a custom field.

    Use this tool to update a recurring expense in Zoho Books by specifying a unique custom field value. If the expense doesn't exist and upsert is enabled, a new expense can be created. This is useful for maintaining accurate financial records.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATERECURRINGEXPENSE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGEXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGEXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringexpenses".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATERECURRINGEXPENSE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": create_new_recurring_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def modify_recurring_expense(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Provide the ID of the organization for which the recurring expense needs to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    recurring_expense_identifier: Annotated[
        str | None,
        "Unique identifier for the recurring expense to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_recurring_expense'."]:
    """Update a recurring expense in Zoho Books.

    Use this tool to modify details of an existing recurring expense in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "MODIFYRECURRINGEXPENSE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not recurring_expense_identifier:
        missing_params.append(("recurring_expense_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["MODIFYRECURRINGEXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["MODIFYRECURRINGEXPENSE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringexpenses/{recurring_expense_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_expense_id=recurring_expense_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["MODIFYRECURRINGEXPENSE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_recurring_expense_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "Provide the ID of the organization to retrieve its specific recurring expense details from Zoho Books.",  # noqa: E501
    ],
    recurring_expense_id: Annotated[
        str, "Unique identifier for the recurring expense to retrieve its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_recurring_expense'."]:
    """Get details of a specific recurring expense in Zoho Books.

    Use this tool to retrieve information about a particular recurring expense by providing its ID. Helpful for managing or reviewing ongoing expense details."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringexpenses/{recurring_expense_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_expense_id=recurring_expense_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_recurring_expense(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    recurring_expense_id: Annotated[
        str, "The unique identifier for the recurring expense to be deleted in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_recurring_expense'."]:
    """Delete an existing recurring expense in Zoho Books.

    Use this tool to delete a recurring expense from Zoho Books when it's no longer needed or has been replaced. The tool confirms the successful removal of the specified recurring expense."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringexpenses/{recurring_expense_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_expense_id=recurring_expense_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def stop_recurring_expense(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books for which the recurring expense will be stopped.",  # noqa: E501
    ],
    recurring_expense_identifier: Annotated[
        str, "Unique identifier for the recurring expense to be stopped."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'stop_recurring_expense'."]:
    """Stop an active recurring expense in Zoho Books.

    This tool is used to stop an active recurring expense in Zoho Books. It should be called when a user wants to terminate a recurring payment schedule associated with an expense."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringexpenses/{recurring_expense_id}/status/stop".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_expense_id=recurring_expense_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def resume_recurring_expense(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    recurring_expense_id: Annotated[
        str, "The unique identifier for the recurring expense to be resumed."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'resume_recurring_expense'."]:
    """Resumes a stopped recurring expense cycle.

    Use this tool to restart a recurring expense that has been previously paused in Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringexpenses/{recurring_expense_id}/status/resume".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_expense_id=recurring_expense_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_child_expenses(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to list child expenses for."
    ],
    recurring_expense_identifier: Annotated[
        str, "Unique identifier for the recurring expense to retrieve child expenses."
    ],
    sort_expenses_by: Annotated[
        str | None,
        "Specify the field to sort expenses. Valid options: next_expense_date, account_name, total, last_created_date, recurrence_name, customer_name, created_time.",  # noqa: E501
    ] = None,
    fetch_page_number: Annotated[
        int | None, "Specify the page number to retrieve. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Specify the number of expense records to retrieve per page. Default is 200."
    ] = 200,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'list_child_expenses_of_recurring_expense'."
]:
    """Retrieve child expenses from a recurring expense.

    Use this tool to list expenses that have been generated from a specific recurring expense. Useful for tracking individual instances of a recurring cost."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringexpenses/{recurring_expense_id}/expenses".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_expense_id=recurring_expense_identifier,
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "sort_column": sort_expenses_by,
            "page": fetch_page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.expenses.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_recurring_expense_history(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the recurring expense history is requested."
    ],
    recurring_expense_id: Annotated[
        str,
        "Unique identifier for the specific recurring expense to retrieve history and comments.",
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_recurring_expense_history'."]:
    """Get history and comments of a recurring expense.

    Use this tool to retrieve the history and comments associated with a specific recurring expense in Zoho Books. Call this tool when you need detailed information about past actions and notes on a recurring expense item."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringexpenses/{recurring_expense_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_expense_id=recurring_expense_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_recurring_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the recurring invoice is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_recurring_invoice'."]:
    """Create a new recurring invoice in Zoho Books.

    This tool creates a new recurring invoice within Zoho Books. Use this tool when you need to automate the billing of customers on a regular schedule. It returns the details of the invoice created, enabling tracking and management of recurring payments.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATERECURRINGINVOICE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATERECURRINGINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATERECURRINGINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringinvoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATERECURRINGINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_recurring_invoices(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization for which you want to list the recurring invoices. Required for accessing specific organization's data.",  # noqa: E501
    ],
    recurrence_unique_name: Annotated[
        str | None,
        "Unique name for the recurring profile, provided by the user. Max-length is 100 characters.",  # noqa: E501
    ] = None,
    search_by_item_name: Annotated[
        str | None,
        "Search recurring invoices by item name, using 'item_name_startswith' or 'profileitemname_contains' variants.",  # noqa: E501
    ] = None,
    item_description_filter: Annotated[
        str | None,
        "Search for recurring invoices by item description using 'startswith' or 'contains' criteria.",  # noqa: E501
    ] = None,
    customer_name: Annotated[
        str | None,
        "Name of the customer for whom the recurring invoice is raised. Use this to filter invoices by customer.",  # noqa: E501
    ] = None,
    line_item_id: Annotated[
        str | None, "Specify the line item ID for filtering recurring invoices."
    ] = None,
    item_id: Annotated[
        str | None, "Unique identifier for the item associated with the recurring invoice."
    ] = None,
    tax_identifier: Annotated[
        str | None, "ID of the tax or tax group associated with the recurring invoice."
    ] = None,
    invoice_note: Annotated[
        str | None,
        "A short note for the recurring invoice, providing additional details or context.",
    ] = None,
    recurring_invoice_start_date: Annotated[
        str | None, "The date on which the recurring invoice starts. Format: YYYY-MM-DD."
    ] = None,
    recurring_invoice_end_date: Annotated[
        str | None, "The date when the recurring invoice expires, formatted as YYYY-MM-DD."
    ] = None,
    customer_id: Annotated[
        str | None,
        "The ID of the customer for whom the recurring invoice is raised. Use this to filter invoices specific to a customer.",  # noqa: E501
    ] = None,
    recurring_invoice_status: Annotated[
        str | None, "Status of the recurring invoice: 'active', 'stopped', or 'expired'."
    ] = None,
    filter_recurring_invoice_status: Annotated[
        str | None,
        "Filter recurring invoices by status or payment expected date. Allowed values: Status.All, Status.Active, Status.Stopped, Status.Expired.",  # noqa: E501
    ] = None,
    search_text: Annotated[
        str | None,
        "Search invoices by invoice number, purchase order, or customer name. Maximum length is 100 characters.",  # noqa: E501
    ] = None,
    sort_by_column: Annotated[
        str | None,
        "Specify the column to sort the recurring invoices by. Leave empty for no sorting.",
    ] = None,
    page_number: Annotated[int | None, "The page number to fetch, with a default value of 1."] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to retrieve per page, with a default of 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_recurring_invoices'."]:
    """Retrieve details of all recurring invoices.

    Use this tool to get detailed information on all recurring invoices. Ideal for managing invoicing schedules and monitoring recurring payments."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringinvoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "recurrence_name": recurrence_unique_name,
            "item_name": search_by_item_name,
            "item_description": item_description_filter,
            "customer_name": customer_name,
            "line_item_id": line_item_id,
            "item_id": item_id,
            "tax_id": tax_identifier,
            "notes": invoice_note,
            "start_date": recurring_invoice_start_date,
            "end_date": recurring_invoice_end_date,
            "customer_id": customer_id,
            "status": recurring_invoice_status,
            "filter_by": filter_recurring_invoice_status,
            "search_text": search_text,
            "sort_column": sort_by_column,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_recurring_invoice_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. This ID is required to update or create a recurring invoice using the custom field.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "The unique API name of the custom field used to identify which recurring invoice to update or create.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "The unique value for the custom field used to identify and update the recurring invoice. This should be a unique string associated with a custom field configured to reject duplicates.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    enable_upsert: Annotated[
        bool | None,
        "Set to true to create a new invoice if no existing invoice matches the unique identifier. Set to false to update only without creating a new invoice.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_recurring_invoice_using_custom_field'."
]:
    """Update or create a recurring invoice using a custom field.

    This tool updates an existing recurring invoice or creates a new one using a custom field's unique value. Use when you need to update invoices based on unique custom identifiers. If the unique key isn't found and the upsert option is true, a new invoice is created.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATERECURRINGINVOICECUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICECUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICECUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringinvoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICECUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": enable_upsert,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_recurring_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books. Required for updating a recurring invoice.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    recurring_invoice_id: Annotated[
        str | None,
        "Unique identifier of the recurring invoice to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_recurring_invoice'."]:
    """Update details of a recurring invoice in Zoho Books.

    Use this tool to modify an existing recurring invoice in Zoho Books by specifying the invoice ID.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATERECURRINGINVOICE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not recurring_invoice_id:
        missing_params.append(("recurring_invoice_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringinvoices/{recurring_invoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_invoice_id=recurring_invoice_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_recurring_invoice_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to access organization-specific data.",  # noqa: E501
    ],
    recurring_invoice_identifier: Annotated[
        str, "Unique identifier for the recurring invoice to retrieve its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_recurring_invoice'."]:
    """Retrieve details of a specific recurring invoice.

    Use this tool to access detailed information about a recurring invoice using its ID. It's useful for financial tracking and management purposes."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringinvoices/{recurring_invoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_invoice_id=recurring_invoice_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_recurring_invoice(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization to which the recurring invoice belongs. Required for identifying the correct organization.",  # noqa: E501
    ],
    recurring_invoice_id: Annotated[
        str, "Unique identifier for the recurring invoice to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_recurring_invoice'."]:
    """Delete an existing recurring invoice.

    Call this tool to remove a recurring invoice from the system. Ensure you have the correct invoice ID to successfully delete the desired invoice."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringinvoices/{recurring_invoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_invoice_id=recurring_invoice_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def stop_recurring_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the recurring invoice is to be stopped.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    recurring_invoice_id: Annotated[
        str | None,
        "The unique identifier for the recurring invoice to be stopped.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'stop_recurring_invoice'."]:
    """Stop an active recurring invoice in Zoho Books.

    Use this tool to stop an active recurring invoice by providing the recurring invoice ID. This tool is useful for managing billing cycles when you need to halt a recurring charge.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["STOPRECURRINGINVOICE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not recurring_invoice_id:
        missing_params.append(("recurring_invoice_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["STOPRECURRINGINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["STOPRECURRINGINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringinvoices/{recurring_invoice_id}/status/stop".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_invoice_id=recurring_invoice_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["STOPRECURRINGINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def resume_recurring_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization whose invoice needs to be resumed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    recurring_invoice_id: Annotated[
        str | None,
        "Unique identifier of the recurring invoice to be resumed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'resume_recurring_invoice'."]:
    """Resumes a stopped recurring invoice.

    Use this tool to resume a recurring invoice that has been paused.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "RESUMERECURRINGINVOICE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not recurring_invoice_id:
        missing_params.append(("recurring_invoice_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["RESUMERECURRINGINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["RESUMERECURRINGINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringinvoices/{recurring_invoice_id}/status/resume".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_invoice_id=recurring_invoice_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["RESUMERECURRINGINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_recurring_invoice_template(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique identifier of the organization. This ID is used to specify which organization's recurring invoice template will be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    recurring_invoice_identifier: Annotated[
        str | None,
        "Unique identifier of the recurring invoice to update the PDF template for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    invoice_template_id: Annotated[
        str | None,
        "Unique identifier of the recurring invoice template to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_recurring_invoice_template'."
]:
    """Update the PDF template for a recurring invoice.

    This tool updates the PDF template associated with a specific recurring invoice in Zoho Books. It is used when you need to change the template for invoices that are generated on a recurring basis.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATERECURRINGINVOICETEMPLATE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not recurring_invoice_identifier:
        missing_params.append(("recurring_invoice_identifier", "path"))
    if not invoice_template_id:
        missing_params.append(("invoice_template_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICETEMPLATE_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICETEMPLATE_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/recurringinvoices/{recurring_invoice_id}/templates/{template_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_invoice_id=recurring_invoice_identifier,
            template_id=invoice_template_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATERECURRINGINVOICETEMPLATE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_recurring_invoice_history(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization. Required to access invoice history."
    ],
    recurring_invoice_id: Annotated[
        str,
        "Unique identifier for the specific recurring invoice to retrieve its history and comments.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_recurring_invoice_history'."]:
    """Get the complete history and comments of a recurring invoice.

    Use this tool to retrieve detailed history and comment logs for a specific recurring invoice. Ideal for reviewing past transactions and communication related to an invoice."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/recurringinvoices/{recurring_invoice_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            recurring_invoice_id=recurring_invoice_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_retainer_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization for which the retainer invoice is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to ignore automatic invoice number generation and manually input the invoice number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_retainer_invoice'."]:
    """Create a retainer invoice for a customer.

    Use this tool to create a retainer invoice for a customer through Zoho Books. It should be called when you need to generate an invoice requiring advance payment or retaining fees for services or products.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATERETAINERINVOICE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATERETAINERINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATERETAINERINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/retainerinvoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATERETAINERINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "ignore_auto_number_generation": ignore_auto_number_generation,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_retainer_invoices(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization within Zoho Books."
    ],
    sort_by_column: Annotated[
        str | None,
        "Specifies the column to sort retainer invoices by. Allowed values: 'customer_name', 'retainer_invoice_number', 'date', 'due_date', 'total', 'balance', 'created_time'.",  # noqa: E501
    ] = None,
    filter_invoices_by_status_or_date: Annotated[
        str | None,
        "Filter invoices by status or payment expected date. Valid values: Status.All, Status.Sent, Status.Draft, Status.OverDue, Status.Paid, Status.Void, Status.Unpaid, Status.PartiallyPaid, Status.Viewed, Date.PaymentExpectedDate.",  # noqa: E501
    ] = None,
    sorting_order: Annotated[
        str | None,
        "The order for sorting retainer invoices. Typically 'asc' for ascending or 'desc' for descending.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None, "Specifies the page number for pagination when listing retainer invoices."
    ] = None,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Default is 200."
    ] = 200,
    print_pdf: Annotated[
        bool | None, "Set to true to print the exported PDF of retainer invoices."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_retainer_invoices'."]:
    """List all retainer invoices with pagination.

    Retrieve a complete list of retainer invoices. Use this tool to access invoices with pagination support."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "print": print_pdf,
            "sort_column": sort_by_column,
            "filter_by": filter_invoices_by_status_or_date,
            "sort_order": sorting_order,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def modify_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization within Zoho Books to which the retainer invoice belongs. This is required to ensure the update is applied to the correct entity.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    retainer_invoice_id: Annotated[
        str | None,
        "Unique identifier of the retainer invoice to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_retainer_invoice'."]:
    """Update an existing invoice in Zoho Books.

    This tool updates the information of an existing retainer invoice in Zoho Books. Use it when you need to modify invoice details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["MODIFYINVOICE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not retainer_invoice_id:
        missing_params.append(("retainer_invoice_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["MODIFYINVOICE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["MODIFYINVOICE_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["MODIFYINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_retainer_invoice_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier of the organization to retrieve the retainer invoice for."
    ],
    retainer_invoice_id: Annotated[
        str, "Unique identifier of the retainer invoice to retrieve details for."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_retainer_invoice'."]:
    """Retrieve details of a specific retainer invoice.

    Use this tool to obtain details about a retainer invoice by specifying the invoice ID. Ideal for checking invoice status, amount, and related information."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_retainer_invoice(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization whose retainer invoice you want to delete."
    ],
    retainer_invoice_identifier: Annotated[
        str,
        "Unique identifier of the retainer invoice to delete. Required for specifying the invoice to be removed.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_retainer_invoice'."]:
    """Delete an existing retainer invoice.

    Use this tool to delete a retainer invoice. Note that invoices with applied payments or credit notes cannot be deleted."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_invoice_sent(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. This ID is necessary to specify which organization's invoice should be marked as sent.",  # noqa: E501
    ],
    retainer_invoice_id: Annotated[
        str, "Unique identifier of the retainer invoice to be marked as sent."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_retainer_invoice_sent'."]:
    """Marks a draft retainer invoice as sent.

    Use this tool to update the status of a draft retainer invoice to 'sent' in Zoho Books. This is useful when you need to track the invoice as having been dispatched to a client."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/status/sent".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def modify_retainer_invoice_template(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The ID of the organization to update the retainer invoice template for. It should be a string representing the organization's unique identifier in Zoho Books.",  # noqa: E501
    ],
    retainer_invoice_id: Annotated[
        str, "Unique identifier for the retainer invoice to update the PDF template."
    ],
    retainer_invoice_template_id: Annotated[
        str, "Unique identifier of the retainer invoice template."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_retainer_invoice_template'."
]:
    """Update the PDF template for a retainer invoice.

    Use this tool to update the PDF template associated with a specific retainer invoice by providing the retainer invoice ID and the template ID. It should be called when you need to change the appearance or format of a retainer invoice."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/templates/{template_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
            template_id=retainer_invoice_template_id,
        ),
        method="PUT",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def void_retainer_invoice(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization required to identify which organization's invoice to void."
    ],
    retainer_invoice_id: Annotated[
        str, "Unique identifier for the retainer invoice to be marked as void."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_retainer_invoice_void'."]:
    """Mark a retainer invoice as void.

    This tool marks a specified retainer invoice as void, disassociating any payments and credits and moving them under customer credits."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/status/void".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_retainer_invoice_as_draft(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization in Zoho Books."],
    retainer_invoice_id: Annotated[
        str, "Unique identifier of the retainer invoice to be marked as draft."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_retainer_invoice_draft'."]:
    """Mark a voided retainer invoice as draft.

    Use this tool to change the status of a voided retainer invoice back to draft. This is useful when you need to reuse or edit a voided invoice within Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{reatinerinvoice_id}/status/draft".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            reatinerinvoice_id=retainer_invoice_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def submit_retainer_invoice(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization in Zoho Books to which the retainer invoice belongs."
    ],
    retainer_invoice_unique_id: Annotated[
        str, "Unique identifier of the retainer invoice for submission."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'submit_retainer_invoice'."]:
    """Submit a retainer invoice for approval in Zoho Books.

    Use this tool to submit a specific retainer invoice for approval in Zoho Books. Call this tool when you need to send a retainer invoice for review and approval."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{reatinerinvoice_id}/submit".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            reatinerinvoice_id=retainer_invoice_unique_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def approve_retainer_invoice(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the retainer invoice is being approved."
    ],
    retainer_invoice_id: Annotated[str, "Unique identifier of the retainer invoice to approve."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approve_retainer_invoice'."]:
    """Approve a retainer invoice in Zoho Books.

    Use this tool to approve a specific retainer invoice in Zoho Books when you have the invoice ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{reatinerinvoice_id}/approve".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            reatinerinvoice_id=retainer_invoice_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def email_retainer_invoice_to_customer(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    retainer_invoice_id: Annotated[
        str | None,
        "The unique identifier of the retainer invoice to be emailed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    email_attachments: Annotated[
        str | None,
        "List of file paths or URLs for files to attach to the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    send_customer_statement: Annotated[
        bool | None,
        "Set to true to send the customer statement PDF with the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    attach_invoice_to_email: Annotated[
        bool | None,
        "Attach the retainer invoice to the email if true. Accepts a boolean value.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_retainer_invoice'."]:
    """Send a retainer invoice to a customer via email.

    This tool emails a retainer invoice to the specified customer. If no custom content is provided, the email will use the default content.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "EMAILRETAINERINVOICETOCUSTOMER_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not retainer_invoice_id:
        missing_params.append(("retainer_invoice_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["EMAILRETAINERINVOICETOCUSTOMER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["EMAILRETAINERINVOICETOCUSTOMER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["EMAILRETAINERINVOICETOCUSTOMER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "send_customer_statement": send_customer_statement,
            "send_attachment": attach_invoice_to_email,
            "attachments": email_attachments,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_retainer_invoice_email_content(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization. Required to access retainer invoice emails."
    ],
    retainer_invoice_id: Annotated[
        str, "Unique identifier of the retainer invoice. Used to fetch the specific email content."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_retainer_invoice_email'."]:
    """Retrieve the email content of a retainer invoice.

    This tool fetches the email content of a specific retainer invoice using the provided invoice ID. It should be called when you need to view or send the email details of a retainer invoice."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_billing_address_retainer_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Required to specify which organization's invoice needs updating.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    retainer_invoice_id: Annotated[
        str | None,
        "Unique identifier of the retainer invoice to update the billing address.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_retainer_invoice_billing_address'."
]:
    """Update billing address for a retainer invoice.

    This tool updates the billing address associated with a specific retainer invoice. It should be used when there's a need to change the billing address for a particular invoice without affecting other records.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEBILLINGADDRESSRETAINERINVOICE_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not retainer_invoice_id:
        missing_params.append(("retainer_invoice_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBILLINGADDRESSRETAINERINVOICE_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEBILLINGADDRESSRETAINERINVOICE_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/address/billing".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEBILLINGADDRESSRETAINERINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_retainer_invoice_templates(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization to retrieve retainer invoice templates from."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_retainer_invoice_templates'."]:
    """Retrieve all retainer invoice PDF templates.

    Call this tool to get a list of all available retainer invoice PDF templates from Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/templates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_retainer_invoice_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization whose invoice attachment you want to retrieve.",
    ],
    retainer_invoice_id: Annotated[
        str,
        "Unique identifier of the retainer invoice for which the attachment is to be retrieved.",
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_retainer_invoice_attachment'."]:
    """Retrieve the file attached to a retainer invoice.

    Use this tool to get the file that has been attached to a specific retainer invoice. It is helpful when you need to view or download the attached document related to the invoice."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def attach_file_to_invoice(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization to which the invoice belongs. Required to specify the correct entity for file attachment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    retainer_invoice_identifier: Annotated[
        str | None,
        "Unique identifier of the retainer invoice to which the file will be attached.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_retainer_invoice_attachment'."]:
    """Attach a file to an invoice.

    Use this tool to upload and attach a file to a specified retainer invoice. This is useful for adding supporting documents or additional information to invoices.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ATTACHFILETOINVOICE_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not retainer_invoice_identifier:
        missing_params.append(("retainer_invoice_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ATTACHFILETOINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ATTACHFILETOINVOICE_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ATTACHFILETOINVOICE_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_retainer_invoice_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books whose invoice attachment is to be deleted.",  # noqa: E501
    ],
    retainer_invoice_id: Annotated[
        str,
        "Unique identifier of the retainer invoice to specify which invoice's attachment should be deleted.",  # noqa: E501
    ],
    document_id: Annotated[
        str, "Unique identifier of the retainer invoice document to be deleted."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'delete_retainer_invoice_attachment'."
]:
    """Delete a file attached to a retainer invoice.

    Use this tool to delete a specific file attached to a retainer invoice. Call it when you need to remove an attachment from an invoice in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/documents/{document_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
            document_id=document_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_retainer_invoice_history(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to fetch its retainer invoice history."
    ],
    retainer_invoice_id: Annotated[
        str, "Unique identifier of the retainer invoice to look up its history and comments."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_retainer_invoice'."]:
    """Get the history and comments of a retainer invoice.

    Fetches complete history and comments for a specific retainer invoice by ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_retainer_invoice_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique ID string of the organization in Zoho Books to add a comment to a retainer invoice.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    retainer_invoice_id: Annotated[
        str | None,
        "A unique identifier for the retainer invoice you want to comment on.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_retainer_invoice_comment'."]:
    """Add a comment to a specific retainer invoice.

    This tool is used to add a comment to a specified retainer invoice in Zoho Books. It should be called when a user wishes to provide additional information or notes related to an existing retainer invoice.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "ADDRETAINERINVOICECOMMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not retainer_invoice_id:
        missing_params.append(("retainer_invoice_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDRETAINERINVOICECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDRETAINERINVOICECOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDRETAINERINVOICECOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_retainer_invoice_comment(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID of the organization within Zoho Books for which the comment is to be deleted.",  # noqa: E501
    ],
    retainer_invoice_id: Annotated[
        str,
        "Unique identifier of the retainer invoice to find the specific invoice for comment deletion.",  # noqa: E501
    ],
    comment_identifier: Annotated[
        str, "Unique identifier of the comment to be deleted from the retainer invoice."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_retainer_invoice_comment'."]:
    """Remove a specific comment from a retainer invoice.

    Use this tool to delete a comment from a retainer invoice in Zoho Books by providing the retainer invoice ID and the comment ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
            comment_id=comment_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_retainer_invoice_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization to which the retainer invoice belongs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    retainer_invoice_id: Annotated[
        str | None,
        "Unique identifier of the retainer invoice to update the comment for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    comment_identifier: Annotated[
        str | None,
        "The unique identifier of the comment to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_retainer_invoice_comment'."]:
    """Update a comment on a retainer invoice.

    Use this tool to update an existing comment of a retainer invoice by specifying the invoice and comment IDs.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATERETAINERINVOICECOMMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not retainer_invoice_id:
        missing_params.append(("retainer_invoice_id", "path"))
    if not comment_identifier:
        missing_params.append(("comment_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERETAINERINVOICECOMMENT_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATERETAINERINVOICECOMMENT_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/retainerinvoices/{retainerinvoice_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            retainerinvoice_id=retainer_invoice_id,
            comment_id=comment_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATERETAINERINVOICECOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_sales_order(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization for which the sales order is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    total_number_of_files: Annotated[
        int | None,
        "Specify the total number of files to be attached to the sales order.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    document_attachment: Annotated[
        str | None,
        "A document to be attached to the sales order. Provide as a string containing the document details or content.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to ignore auto sales order number generation, requiring manual sales order number entry.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    can_send_via_email: Annotated[
        bool | None,
        "Set to true if the file can be sent via email.  Only used when mode is 'execute'.",
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_sales_order'."]:
    """Create a sales order for a customer.

    This tool creates a sales order for a customer using Zoho Books. It should be called when you need to generate a new sales order in your accounting system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATESALESORDER_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATESALESORDER_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATESALESORDER_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATESALESORDER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_identifier,
            "ignore_auto_number_generation": ignore_auto_number_generation,
            "can_send_in_mail": can_send_via_email,
            "totalFiles": total_number_of_files,
            "doc": document_attachment,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_sales_orders(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which sales orders are to be listed."
    ],
    sort_column: Annotated[
        str | None,
        "Column field to sort sales order results. Options: customer_name, salesorder_number, shipment_date, last_modified_time, reference_number, total, date, created_time.",  # noqa: E501
    ] = None,
    cross_field_search_text: Annotated[
        str | None,
        "A general search term for matching text across fields like sales order number, reference number, and customer name for quick identification.",  # noqa: E501
    ] = None,
    filter_sales_order_by_status: Annotated[
        str | None,
        "Filter sales orders by status. Options: All, Open, Draft, OverDue, PartiallyInvoiced, Invoiced, Void, Closed.",  # noqa: E501
    ] = None,
    filter_by_sales_order_number: Annotated[
        str | None,
        "Filter sales orders by sales order number with operators: startswith, not_in, in, or contains. Max length: 100 characters.",  # noqa: E501
    ] = None,
    filter_by_item_name: Annotated[
        str | None,
        "Filter sales orders by line item name. Use matching operators like startswith, not_in, in, and contains. Max length: 100 characters.",  # noqa: E501
    ] = None,
    filter_by_item_id: Annotated[
        str | None,
        "Filter sales orders by a specific line item identifier to retrieve orders containing a particular product or service.",  # noqa: E501
    ] = None,
    filter_by_item_description: Annotated[
        str | None,
        "Filter sales orders by line item description. Supports variants like startswith, not_in, in, and contains. Max length: 100 characters.",  # noqa: E501
    ] = None,
    filter_by_reference_number: Annotated[
        str | None,
        "Filter sales orders by external reference number using operators like `startswith`, `not_in`, `in`, and `contains`.",  # noqa: E501
    ] = None,
    customer_name_filter: Annotated[
        str | None,
        "Filter sales orders by customer name with operators like startswith, not_in, in, and contains. Max length: 100 characters.",  # noqa: E501
    ] = None,
    filter_by_total: Annotated[
        float | None,
        "Specify range operators to filter sales orders by total. Use total_start, total_end, total_less_than, total_greater_than, etc.",  # noqa: E501
    ] = None,
    creation_date_filter: Annotated[
        str | None,
        "Filter sales orders by creation date using operators like `date_start`, `date_end`, `date_before`, `date_after`. Format: `yyyy-mm-dd`.",  # noqa: E501
    ] = None,
    shipment_date_filter: Annotated[
        str | None,
        "Specify the shipment date filter for sales orders. Use variants such as 'shipment_date_start', 'shipment_date_end', 'shipment_date_before', and 'shipment_date_after' in 'yyyy-mm-dd' format.",  # noqa: E501
    ] = None,
    sales_order_status: Annotated[
        str | None,
        "Filter sales orders by their status. Allowed values: draft, open, invoiced, partially_invoiced, void, and overdue.",  # noqa: E501
    ] = None,
    filter_by_customer_id: Annotated[
        str | None,
        "Filter sales orders by specific customer ID. Retrieves orders associated with a customer for CRM and reporting.",  # noqa: E501
    ] = None,
    sales_representative_id: Annotated[
        str | None,
        "Filter sales orders by specific sales representative ID for tracking and reporting purposes.",  # noqa: E501
    ] = None,
    sales_order_ids: Annotated[
        str | None,
        "Comma-separated list of sales order IDs to filter results. Maximum length is 200 characters.",  # noqa: E501
    ] = None,
    last_modified_time: Annotated[
        str | None,
        "Specify the last modified time of the sales order to filter results. Use the format 'yyyy-mm-dd'.",  # noqa: E501
    ] = None,
    response_format: Annotated[
        str | None,
        "Specifies the format for sales order details. Must be one of: json, csv, xml, xls, xlsx, pdf, jhtml, or html. Default is json.",  # noqa: E501
    ] = None,
    custom_view_id: Annotated[
        str | None, "ID of the custom view to filter sales orders based on predefined criteria."
    ] = None,
    deal_crm_potential_id: Annotated[
        int | None,
        "Potential ID of a Deal in CRM. Used to filter sales orders associated with a specific deal.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None,
        "Specify the page number for retrieving paginated sales order results. Default is 1 for the first page.",  # noqa: E501
    ] = 1,
    max_sales_orders_per_page: Annotated[
        int | None,
        "Specify the maximum number of sales order records to return per page. Default is 200 for optimal performance and memory usage.",  # noqa: E501
    ] = 200,
    enable_printing: Annotated[
        bool | None,
        "Enable printing of the exported PDF. Use when 'accept' is set to 'pdf' and 'salesorder_ids' includes values.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_sales_orders'."]:
    """Retrieve a list of all sales orders.

    Use this tool to get a complete list of sales orders. It helps in managing and reviewing sales transactions efficiently."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "sort_column": sort_column,
            "search_text": cross_field_search_text,
            "filter_by": filter_sales_order_by_status,
            "salesorder_number": filter_by_sales_order_number,
            "item_name": filter_by_item_name,
            "item_id": filter_by_item_id,
            "item_description": filter_by_item_description,
            "reference_number": filter_by_reference_number,
            "customer_name": customer_name_filter,
            "total": filter_by_total,
            "date": creation_date_filter,
            "shipment_date": shipment_date_filter,
            "status": sales_order_status,
            "customer_id": filter_by_customer_id,
            "salesperson_id": sales_representative_id,
            "salesorder_ids": sales_order_ids,
            "last_modified_time": last_modified_time,
            "accept": response_format,
            "print": enable_printing,
            "customview_id": custom_view_id,
            "zcrm_potential_id": deal_crm_potential_id,
            "page": page_number,
            "per_page": max_sales_orders_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_sales_order_with_custom_field(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_api_name: Annotated[
        str | None,
        "The API name of the unique custom field used to identify the sales order to update or create.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    unique_custom_field_value: Annotated[
        str | None,
        "The unique value of the custom field used to retrieve and update a specific sales order.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    allow_creation_if_not_found: Annotated[
        bool | None,
        "Set to true to create a new sales order if the unique custom field value is not found. Complete details are required for creation.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_sales_order_using_custom_field'."
]:
    """Update or create a sales order using a custom field.

    Use a custom field's unique value to update an existing sales order or create a new one if the order does not exist and the required details are provided. This tool is useful when managing sales orders using specific identifiers in Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATESALESORDERWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATESALESORDERWITHCUSTOMFIELD_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": unique_custom_field_api_name,
            "X-Unique-Identifier-Value": unique_custom_field_value,
            "X-Upsert": allow_creation_if_not_found,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_sales_order_in_zoho_books(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization to which the sales order belongs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_id: Annotated[
        str | None,
        "Unique identifier of the sales order to update in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    total_number_of_files: Annotated[
        int | None,
        "Specify the total number of files to be attached to the sales order update.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    attach_document: Annotated[
        str | None,
        "A document to be attached to the sales order. Provide the file path or URL as a string.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to TRUE to ignore auto-generation of the sales order number. This requires manually entering the number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    allow_email_sending: Annotated[
        bool | None,
        "Determine if the updated sales order can be sent via email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_sales_order'."]:
    """Update details of an existing sales order in Zoho Books.

    Use this tool to update the details of an existing sales order in Zoho Books. It also allows for the deletion of line items by omitting them from the update request.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATESALESORDERINZOHOBOOKS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not sales_order_id:
        missing_params.append(("sales_order_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERINZOHOBOOKS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERINZOHOBOOKS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATESALESORDERINZOHOBOOKS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "ignore_auto_number_generation": ignore_auto_number_generation,
            "can_send_in_mail": allow_email_sending,
            "totalFiles": total_number_of_files,
            "doc": attach_document,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_sales_order_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. This ID is used to specify the organization within Zoho Books whose sales order details need to be retrieved.",  # noqa: E501
    ],
    sales_order_id: Annotated[
        str, "Unique identifier of the sales order required to retrieve its details."
    ],
    output_format: Annotated[
        str | None,
        "Specifies the format in which to receive the sales order details. Options include: json, csv, xml, xls, xlsx, pdf, jhtml, and html. Default is json.",  # noqa: E501
    ] = None,
    print_pdf: Annotated[
        bool | None, "Set to true to print the exported PDF of the sales order, otherwise false."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_sales_order'."]:
    """Retrieve details of a specific sales order.

    This tool is used to get the details of a sales order from Zoho Books. It should be called when there's a need to look up information about a particular sales order, such as for verification or record-keeping purposes."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "print": print_pdf,
            "accept": output_format,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_sales_order(
    context: ToolContext,
    organization_id: Annotated[
        str, "Provide the ID of the organization for which the sales order will be deleted."
    ],
    sales_order_id: Annotated[
        str, "Unique identifier for the sales order to be deleted. Ensure it is not invoiced."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_sales_order'."]:
    """Delete an existing sales order.

    Use this tool to delete an existing sales order by its ID. It cannot delete invoiced sales orders."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_sales_order_custom_fields(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization associated with the sales order. This is required to identify which organization's sales order needs updating.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_id: Annotated[
        str | None,
        "Unique identifier for the sales order to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_salesorder_customfields'."]:
    """Update custom fields in existing sales orders efficiently.

    Call this tool to update the value of custom fields in existing sales orders when you need to modify order details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATESALESORDERCUSTOMFIELDS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not sales_order_id:
        missing_params.append(("sales_order_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERCUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERCUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorder/{salesorder_id}/customfields".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATESALESORDERCUSTOMFIELDS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def open_sales_order(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization where the sales order is to be marked as open."
    ],
    sales_order_id: Annotated[
        str, "Unique identifier of the sales order to mark as open in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_sales_order_as_open'."]:
    """Mark a draft sales order as open in Zoho Books.

    Use this tool to change the status of a draft sales order to open in Zoho Books. This is useful when a sales order needs to be moved from draft status to active status, indicating it's ready for further processing."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/status/open".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_sales_order_as_void(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_id: Annotated[
        str | None,
        "Unique identifier for the specific sales order to be marked as void.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_sales_order_as_void'."]:
    """Mark a sales order as void in Zoho Books.

    Use this tool to transition a specified sales order to a void status, effectively canceling it within Zoho Books.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["MARKSALESORDERASVOID_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not sales_order_id:
        missing_params.append(("sales_order_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["MARKSALESORDERASVOID_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["MARKSALESORDERASVOID_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/status/void".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["MARKSALESORDERASVOID_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_sales_order_sub_status(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique ID of the organization in Zoho Books. This ID is required to specify which organization's sales order needs an update.",  # noqa: E501
    ],
    sales_order_id: Annotated[str, "Unique identifier for the specific sales order to update."],
    sales_order_status_code: Annotated[
        str,
        "The unique code representing the new status for a sales order. This is required to update the status in Zoho Books.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_sales_order_sub_status'."]:
    """Update the sub status of a sales order in Zoho Books.

    This tool updates the sub status of a specified sales order in Zoho Books, identified by the sales order ID and the new status code. It should be called when you need to change the status of an existing sales order."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/substatus/{status_code}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            salesorder_id=sales_order_id,
            status_code=sales_order_status_code,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def email_sales_order_to_customer(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization for which the sales order is being emailed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_id: Annotated[
        str | None,
        "Provide the unique identifier of the sales order to be emailed to the customer.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_identifier: Annotated[
        str | None,
        "Unique identifier of the sales order to be emailed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_attachments: Annotated[
        str | None,
        "A list of file paths or URLs for attachments to include with the sales order email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    file_name: Annotated[
        str | None,
        "Specify the name of the file to be attached to the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    include_sales_order_attachment: Annotated[
        bool | None,
        "Specify true to include the sales order attachment in the email, or false to exclude it.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_sales_order'."]:
    """Email a sales order to a customer.

    Send an email containing the sales order to the customer, with optional custom content. If no content is provided, the default email content will be used.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "EMAILSALESORDERTOCUSTOMER_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not sales_order_identifier:
        missing_params.append(("sales_order_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["EMAILSALESORDERTOCUSTOMER_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["EMAILSALESORDERTOCUSTOMER_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            salesorder_id=sales_order_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["EMAILSALESORDERTOCUSTOMER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "salesorder_id": sales_order_identifier,
            "attachments": sales_order_attachments,
            "send_attachment": include_sales_order_attachment,
            "file_name": file_name,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_sales_order_email_content(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "ID of the organization in Zoho Books for which the sales order email content is required.",
    ],
    sales_order_id: Annotated[
        str, "Unique identifier of the sales order to retrieve its email content."
    ],
    email_template_id: Annotated[
        str | None,
        "Optional. ID of the email template for retrieving specific email content. If not provided, defaults will be used.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_sales_order_email'."]:
    """Retrieve email content for a specific sales order.

    This tool is used to get the email content associated with a particular sales order, identified by its sales order ID. Use it when you need to view or analyze the email details of a sales order in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "email_template_id": email_template_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def submit_sales_order_for_approval(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization in Zoho Books where the sales order is submitted."
    ],
    sales_order_id: Annotated[
        str, "Unique identifier of the sales order to be submitted for approval."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'submit_sales_order'."]:
    """Submit a sales order for approval in Zoho Books.

    Use this tool to submit a sales order for approval in the Zoho Books system by providing the sales order ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/submit".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def approve_sales_order(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization in Zoho Books required for approving a sales order."
    ],
    sales_order_id: Annotated[str, "The unique identifier for the sales order to be approved."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approve_sales_order'."]:
    """Approve a specified sales order in Zoho Books.

    Use this tool to approve a particular sales order within the Zoho Books system. This is typically called when a sales order needs to be confirmed and finalized."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/approve".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def export_sales_orders_pdf(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization whose sales orders will be exported as a PDF. This ID is required to access and retrieve the sales order data from Zoho Books.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_export_sales_orders_as_pdf'."]:
    """Export sales orders as a single PDF document.

    Use this tool to export up to 25 sales orders into a single PDF file from Zoho Books. It is useful for generating and retrieving sales documentation in bulk for reporting or archiving."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/pdf".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def export_print_sales_orders(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization whose sales orders you want to export and print as PDFs."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_print_sales_orders'."]:
    """Export and print sales orders as PDFs.

    Exports up to 25 sales orders as PDF files for printing. This tool is used to generate printable documents of sales orders in batch format."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/print".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_sales_order_billing_address(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization associated with the sales order.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_identifier: Annotated[
        str | None,
        "Unique identifier of the sales order to update the billing address for.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_sales_order_billing_address'."
]:
    """Updates the billing address for a specific sales order.



    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATESALESORDERBILLINGADDRESS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not sales_order_identifier:
        missing_params.append(("sales_order_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/address/billing".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            salesorder_id=sales_order_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATESALESORDERBILLINGADDRESS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_shipping_address_sales_order(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The ID of the organization in Zoho Books to update the shipping address for the sales order.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_identifier: Annotated[
        str | None,
        "Unique identifier of the sales order to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_sales_order_shipping_address'."
]:
    """Update the shipping address for a specific sales order.

    This tool updates the shipping address for a specific sales order in Zoho Books. It should be called when there's a need to modify the delivery location of an existing order.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATESHIPPINGADDRESSSALESORDER_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not sales_order_identifier:
        missing_params.append(("sales_order_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESHIPPINGADDRESSSALESORDER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESHIPPINGADDRESSSALESORDER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/address/shipping".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            salesorder_id=sales_order_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATESHIPPINGADDRESSSALESORDER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_sales_order_templates(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "A string representing the ID of the organization. Required to specify which organization's sales order templates to retrieve.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_sales_order_templates'."]:
    """Retrieve all sales order PDF templates from Zoho Books.

    Use this tool to get a list of all available sales order PDF templates in Zoho Books. This is helpful when you need to view or manage your sales order templates."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/templates".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_sales_order_template(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization associated with the sales order."
    ],
    sales_order_id: Annotated[
        str, "Unique identifier for the sales order to be updated with a new PDF template."
    ],
    sales_order_template_id: Annotated[
        str, "Unique identifier of the sales order template to update."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_sales_order_template'."]:
    """Update the PDF template for a sales order.

    This tool updates the PDF template associated with a specific sales order. Use it when you need to change the template for an existing sales order."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/templates/{template_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            salesorder_id=sales_order_id,
            template_id=sales_order_template_id,
        ),
        method="PUT",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_sales_order_attachment(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID of the organization in Zoho Books."],
    sales_order_id: Annotated[
        str, "Unique identifier of the sales order to retrieve the attachment for."
    ],
    require_preview_of_sales_order: Annotated[
        bool | None,
        "Specify whether a preview of the Sales Order is required. Use True for preview, False for no preview.",  # noqa: E501
    ] = None,
    require_inline_response: Annotated[
        bool | None,
        "Set to true if an inline response is needed, displaying directly in the browser.",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_sales_order_attachment'."]:
    """Retrieve the file attached to a specific sales order.

    Use this tool to get the file attached to a sales order in Zoho Books by providing the sales order ID. It returns the file associated with the specified order."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "preview": require_preview_of_sales_order,
            "inline": require_inline_response,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def attach_file_to_sales_order(
    context: ToolContext,
    organization_id: Annotated[str, "Unique identifier for the organization in Zoho Books."],
    sales_order_identifier: Annotated[
        str, "Unique identifier of the sales order to which the file will be attached."
    ],
    file_to_attach: Annotated[
        str | None, "Path or identifier of the file to be attached to the sales order."
    ] = None,
    document_file_path: Annotated[
        str | None, "Path to the document file that needs to be attached to the sales order."
    ] = None,
    number_of_files: Annotated[
        int | None, "Specify the total number of files to be attached to the sales order."
    ] = None,
    document_identifiers: Annotated[
        str | None,
        "A string representing the IDs of the documents to attach. Comma-separated for multiple IDs.",  # noqa: E501
    ] = None,
    allow_sending_file_in_mail: Annotated[
        bool | None, "Boolean indicating if the file can be sent in mail. True allows sending."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_sales_order_attachment'."]:
    """Attach a file to a specific sales order in Zoho Books.

    This tool is used to attach a file to a specific sales order in Zoho Books. It should be called when you need to upload and associate a document with a sales order for tracking or record-keeping purposes."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            salesorder_id=sales_order_identifier,
        ),
        method="POST",
        params=remove_none_values({
            "organization_id": organization_id,
            "attachment": file_to_attach,
            "can_send_in_mail": allow_sending_file_in_mail,
            "doc": document_file_path,
            "totalFiles": number_of_files,
            "document_ids": document_identifiers,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def set_sales_order_attachment_preference(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization for which you want to update the attachment preference.",
    ],
    sales_order_id: Annotated[str, "Unique identifier of the sales order to update."],
    allow_attachment_in_email: Annotated[
        bool,
        "Indicate if the file can be sent in the email. Set to true to allow, false to prevent.",
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_attachment_preference1'."]:
    """Sets attachment preference for sales order emails.

    Use this tool to set whether an attached file should be sent when emailing a sales order. Call this when you need to update the attachment settings on a specific sales order."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="PUT",
        params=remove_none_values({
            "organization_id": organization_id,
            "can_send_in_mail": allow_attachment_in_email,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_sales_order_attachment(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization whose sales order attachment is to be deleted."
    ],
    sales_order_id: Annotated[
        str, "Unique identifier for the sales order from which the attachment will be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_sales_order_attachment'."]:
    """Delete an attached file from a sales order in Zoho Books.

    Call this tool to remove an attached file from a specified sales order in Zoho Books. Useful for managing and updating records by deleting unnecessary or outdated attachments."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/attachment".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_sales_order_comments(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization for which to retrieve sales order comments."
    ],
    sales_order_id: Annotated[
        str, "Unique identifier of the sales order to retrieve comments and history for."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_sales_order_comments'."]:
    """Retrieve the history and comments of a sales order.

    Use this tool to obtain all comments and the complete history associated with a specific sales order when needing detailed insights or updates."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_sales_order_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_id: Annotated[
        str | None,
        "Unique identifier of the sales order to which the comment will be added.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_sales_order_comment'."]:
    """Add a comment to a sales order in Zoho Books.

    This tool is used to add a comment to a specific sales order in Zoho Books. It should be called when you need to leave additional remarks or notes on a sales order.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDSALESORDERCOMMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not sales_order_id:
        missing_params.append(("sales_order_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDSALESORDERCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDSALESORDERCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), salesorder_id=sales_order_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDSALESORDERCOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_sales_order_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_order_id: Annotated[
        str | None,
        "Unique identifier of the sales order to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    comment_id: Annotated[
        str | None,
        "Unique identifier of the comment associated with the sales order that needs to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_sales_order_comment'."]:
    """Update an existing comment on a sales order.



    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATESALESORDERCOMMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not sales_order_id:
        missing_params.append(("sales_order_id", "path"))
    if not comment_id:
        missing_params.append(("comment_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESORDERCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            salesorder_id=sales_order_id,
            comment_id=comment_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATESALESORDERCOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.salesorders.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_sales_order_comment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization. This ID is required to specify which organization's sales order comment needs to be deleted.",  # noqa: E501
    ],
    sales_order_id: Annotated[
        str, "Unique identifier of the sales order to delete the comment from."
    ],
    comment_identifier: Annotated[str, "Unique identifier of the comment to delete."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_sales_order_comment'."]:
    """Delete a comment from a sales order in Zoho Books.

    Use this tool to remove a comment from a specific sales order in Zoho Books by providing the sales order ID and the comment ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesorders/{salesorder_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            salesorder_id=sales_order_id,
            comment_id=comment_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_sales_receipt(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique identifier for the organization needed to create the sales receipt.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to ignore automatic sales receipt number generation, requiring manual entry of the receipt number.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    send_receipt_via_email: Annotated[
        bool | None,
        "Set to true to send the sales receipt to the customer via email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_sales_receipt'."]:
    """Create a sales receipt for immediate payment transactions.



    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATESALESRECEIPT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATESALESRECEIPT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATESALESRECEIPT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesreceipts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATESALESRECEIPT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "ignore_auto_number_generation": ignore_auto_number_generation,
            "can_send_in_mail": send_receipt_via_email,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_sales_receipts(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization. Required to list sales receipts."
    ],
    search_receipt_by_number: Annotated[
        str | None,
        "Search for receipts using their unique number. Supports 'startswith' and 'contains' filters. Max length: 100 characters.",  # noqa: E501
    ] = None,
    search_by_item_name: Annotated[
        str | None,
        "Search sales receipts by item name using 'startswith' or 'contains' variants. Maximum length is 100 characters.",  # noqa: E501
    ] = None,
    sort_by_column: Annotated[
        str | None,
        "Specify the column to sort sales receipts by. Options: customer_name, receipt_number, date, total, created_time.",  # noqa: E501
    ] = None,
    filter_sales_receipts_by_status: Annotated[
        str | None,
        "Filter sales receipts based on their status. Options include date ranges like 'ThisWeek', and statuses like 'Status.Draft'.",  # noqa: E501
    ] = None,
    customer_identifier: Annotated[
        str | None,
        "Filter sales receipts by specific customer identifier. Provide the unique ID of the customer to retrieve their sales receipts.",  # noqa: E501
    ] = None,
    date_filter: Annotated[
        str | None,
        "Filter sales receipts by date using variants like date_start, date_end, date_before, and date_after. Use yyyy-mm-dd format.",  # noqa: E501
    ] = None,
    total_filter_options: Annotated[
        float | None,
        "Filter sales receipts using range operators like total_start, total_end, total_less_than, and total_greater_than. Expects a number specifying the total amount.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None,
        "Specify the page number for retrieving paginated sales receipt results. Defaults to 1.",
    ] = 1,
    max_records_per_page: Annotated[
        int | None,
        "Specify the maximum number of sales receipt records to return per page. Default is 200.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_sales_receipts'."]:
    """Retrieve a list of all sales receipts.

    Use this tool to obtain a comprehensive list of all sales receipts. It should be called when there's a need to review or analyze sales transaction records."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesreceipts".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "receipt_number": search_receipt_by_number,
            "item_name": search_by_item_name,
            "sort_column": sort_by_column,
            "filter_by": filter_sales_receipts_by_status,
            "customer_id": customer_identifier,
            "date": date_filter,
            "total": total_filter_options,
            "page": page_number,
            "per_page": max_records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_sales_receipt(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization in Zoho Books. It is required to specify which organization's sales receipt needs to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    sales_receipt_identifier: Annotated[
        str | None,
        "Unique identifier for the sales receipt to be updated. This is required to specify which receipt to modify.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_sales_receipt'."]:
    """Update an existing sales receipt in Zoho Books.

    This tool is used to update details of an existing sales receipt in Zoho Books. It should be called when modifications to sales receipt data are required, such as changing amounts, dates, or other relevant information.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATESALESRECEIPT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not sales_receipt_identifier:
        missing_params.append(("sales_receipt_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESRECEIPT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATESALESRECEIPT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesreceipts/{sales_receipt_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            sales_receipt_id=sales_receipt_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATESALESRECEIPT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_sales_receipt_details(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "ID of the organization for which the sales receipt is being retrieved."
    ],
    sales_receipt_id: Annotated[
        str,
        "The unique identifier for the sales receipt to be retrieved. Required for fetching the specific sales receipt details.",  # noqa: E501
    ],
    output_format: Annotated[
        str | None,
        "Specifies the format in which to retrieve the sales receipt details. Options are 'json', 'pdf', or 'html'. Default is 'json'.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_sales_receipt'."]:
    """Retrieve the details of a sales receipt.

    This tool fetches the specific details of a sales receipt using the provided sales receipt ID. It should be called when detailed information about a particular sales receipt is needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesreceipts/{sales_receipt_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), sales_receipt_id=sales_receipt_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "accept": output_format,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_sales_receipt(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID of the organization in Zoho Books."],
    sales_receipt_id: Annotated[str, "The unique identifier for the sales receipt to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_sales_receipt'."]:
    """Delete an existing sales receipt in Zoho Books.

    This tool deletes a specified sales receipt from Zoho Books. It should be used when you need to permanently remove a sales receipt by its ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/salesreceipts/{sales_receipt_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), sales_receipt_id=sales_receipt_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.invoices.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def email_sales_receipt_to_customer(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    sales_receipt_identifier: Annotated[
        str | None,
        "The unique identifier of the sales receipt to be emailed to the customer. It must be a string.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    attach_pdf: Annotated[
        bool | None,
        "Set to true to send the sales receipt PDF with the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_sales_receipt'."]:
    """Email a sales receipt to the customer.

    Use this tool to send a sales receipt via email to a specific customer. It should be called when you need to deliver the sales receipt directly to the customer.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "EMAILSALESRECEIPTTOCUSTOMER_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not sales_receipt_identifier:
        missing_params.append(("sales_receipt_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["EMAILSALESRECEIPTTOCUSTOMER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["EMAILSALESRECEIPTTOCUSTOMER_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/salesreceipts/{sales_receipt_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            sales_receipt_id=sales_receipt_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["EMAILSALESRECEIPTTOCUSTOMER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"attach_pdf": attach_pdf}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_project_task(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization for which the task is being added.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    project_identifier: Annotated[
        str | None,
        "Unique identifier of the project in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_task'."]:
    """Add a task to a specific project.

    Use this tool to add a task to a specified project in Zoho Books. It is suitable when you need to organize tasks within a project and track them through Zoho's project management system.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["ADDPROJECTTASK_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not project_identifier:
        missing_params.append(("project_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["ADDPROJECTTASK_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["ADDPROJECTTASK_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/{project_id}/tasks".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), project_id=project_identifier
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDPROJECTTASK_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_project_tasks(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. This is required to specify which organization's project tasks to retrieve.",  # noqa: E501
    ],
    project_unique_id: Annotated[str, "Unique identifier for the project to fetch tasks."],
    fetch_page_number: Annotated[
        int | None, "The page number of results to retrieve. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Specify the number of task records to fetch per page. Defaults to 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_tasks'."]:
    """Retrieve a list of tasks for a specified project.

    Use this tool to obtain a list of tasks added to a specific project in Zoho Books. It should be called when you need detailed task information for project management or oversight."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/tasks".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), project_id=project_unique_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": fetch_page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_project_task(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books to identify the context for the task update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    project_identifier: Annotated[
        str | None,
        "Unique identifier of the project in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    task_identifier: Annotated[
        str | None,
        "Unique identifier of the task to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_task'."]:
    """Update the details of a project task.

    Use this tool to update the details of a specific task within a project in Zoho Books. It should be called when you need to change task information such as its status, due date, or other attributes.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEPROJECTTASK_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not project_identifier:
        missing_params.append(("project_identifier", "path"))
    if not task_identifier:
        missing_params.append(("task_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPROJECTTASK_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEPROJECTTASK_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/{project_id}/tasks/{task_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_identifier,
            task_id=task_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEPROJECTTASK_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_task_details(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    project_id: Annotated[str, "Unique identifier of the project."],
    task_unique_identifier: Annotated[
        str, "The unique identifier for the task to retrieve details for from Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_task'."]:
    """Retrieve detailed information about a specific task in a project.

    This tool should be called to obtain comprehensive details about a particular task within a project. It helps in understanding task specifics by providing relevant task data from Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/tasks/{task_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_id,
            task_id=task_unique_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_project_task(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique ID of the organization to which the task belongs."
    ],
    project_identifier: Annotated[
        str, "Unique identifier for the project from which a task will be deleted."
    ],
    task_identifier: Annotated[str, "Unique identifier of the task to be deleted in the project."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_task'."]:
    """Remove a task from a specific project in Zoho Books.

    Call this tool to delete a specified task from a project using Zoho Books API. Use it when you need to manage project tasks and remove any that are no longer relevant."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/{project_id}/tasks/{task_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            project_id=project_identifier,
            task_id=task_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_associated_tax(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization in Zoho Books where the tax will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_tax'."]:
    """Create and associate a tax with an item.

    This tool creates a tax that can be added to an item within the Zoho Books system. It is used when a new tax needs to be defined and linked to specific goods or services.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEASSOCIATEDTAX_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEASSOCIATEDTAX_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEASSOCIATEDTAX_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/taxes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEASSOCIATEDTAX_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_taxes(
    context: ToolContext,
    organization_id: Annotated[str, "Unique ID of the organization to list taxes for."],
    page_number: Annotated[
        int | None, "Page number of the tax list to retrieve. Defaults to 1 if not specified."
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "The number of tax records to retrieve per page. Defaults to 200 if not specified.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_taxes'."]:
    """Retrieve a list of simple and compound taxes.

    Use this tool to get a list of simple and compound taxes available in the system, with options for pagination."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxes".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_tax_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique identifier for the organization that owns the tax to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    tax_identifier: Annotated[
        str | None,
        "Unique identifier of the tax to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_tax'."]:
    """Update the details of a specified tax.

    Use this tool to update the details of a simple or compound tax by providing the tax ID.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATETAXDETAILS_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not tax_identifier:
        missing_params.append(("tax_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATETAXDETAILS_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATETAXDETAILS_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/taxes/{tax_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), tax_id=tax_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATETAXDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_tax_details(
    context: ToolContext,
    organization_id: Annotated[str, "The ID of the organization to retrieve tax details from."],
    tax_identifier: Annotated[str, "Unique identifier for retrieving specific tax details."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_tax'."]:
    """Retrieve the details of a specific tax.

    Use this tool to obtain information about a specific simple or compound tax by providing the tax ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxes/{tax_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), tax_id=tax_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_tax(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The ID of the organization within Zoho Books. This is required to specify which organization's tax entry to delete.",  # noqa: E501
    ],
    tax_identifier: Annotated[str, "Unique identifier of the tax to be deleted in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_tax'."]:
    """Delete a simple or compound tax in Zoho Books.

    Use this tool to remove a specific tax, whether simple or compound, from the Zoho Books system. This is useful when managing taxes and ensuring only relevant ones are active."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxes/{tax_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), tax_id=tax_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def retrieve_tax_group_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization in Zoho Books to get the tax group details for."
    ],
    tax_group_identifier: Annotated[str, "Unique identifier for the tax group in Zoho Books."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_tax_group'."]:
    """Retrieve details of a specific tax group.

    Use this to obtain information about a specific tax group by its ID in Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxgroups/{tax_group_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), tax_group_id=tax_group_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_tax_group_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique ID of the organization in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    tax_group_identifier: Annotated[
        str | None,
        "Unique identifier of the tax group to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_tax_group'."]:
    """Update details of a specific tax group in Zoho Books.

    This tool updates the details of a tax group in Zoho Books using the provided tax group ID. It should be called when changes are needed to the tax group's configuration.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATETAXGROUPDETAILS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not tax_group_identifier:
        missing_params.append(("tax_group_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATETAXGROUPDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATETAXGROUPDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/taxgroups/{tax_group_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), tax_group_id=tax_group_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATETAXGROUPDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_tax_group(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "The unique identifier for the organization in Zoho Books."
    ],
    tax_group_identifier: Annotated[
        str,
        "Unique identifier of the tax group to be deleted. Ensure it's not associated with active transactions to proceed.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_tax_group'."]:
    """Delete a tax group if not associated with transactions.

    Use this tool to delete a tax group from Zoho Books. Note that tax groups associated with transactions cannot be deleted."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxgroups/{tax_group_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), tax_group_id=tax_group_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_tax_group(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization within Zoho Books. This is required to create a tax group.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_tax_group'."]:
    """Create a tax group with multiple associated taxes.

    This tool calls the Zoho Books API to create a tax group, allowing you to associate multiple taxes into a single group. Use this tool when you need to manage and apply tax groups for accounting purposes.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATETAXGROUP_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATETAXGROUP_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["CREATETAXGROUP_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/taxgroups".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATETAXGROUP_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_tax_authority(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the tax authority is to be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_tax_authority'."]:
    """Create a tax authority in Zoho Books.

    Use this tool to create a new tax authority in Zoho Books. Suitable for financial administrators managing tax compliance.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATETAXAUTHORITY_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATETAXAUTHORITY_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATETAXAUTHORITY_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/taxauthorities".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATETAXAUTHORITY_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_tax_authorities(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which to retrieve tax authorities."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_tax_authorities'."]:
    """Retrieve the list of tax authorities.

    Use this tool to obtain a list of all tax authorities, which can be helpful for managing tax-related settings or compliance."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxauthorities".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_tax_authority_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization for which the tax authority details need to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    tax_authority_identifier: Annotated[
        str | None,
        "Unique identifier of the tax authority to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_tax_authority'."]:
    """Update details of a tax authority.

    Use this tool to update information for a specific tax authority. Ideal for modifications to existing records.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATETAXAUTHORITYDETAILS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not tax_authority_identifier:
        missing_params.append(("tax_authority_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATETAXAUTHORITYDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATETAXAUTHORITYDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/taxauthorities/{tax_authority_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            tax_authority_id=tax_authority_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATETAXAUTHORITYDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_tax_authority_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "Provide the ID of the organization to retrieve tax authority details."
    ],
    tax_authority_unique_id: Annotated[
        str, "Unique identifier of the tax authority to retrieve details for."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_tax_authority'."]:
    """Retrieve details of a specific tax authority.

    Use this tool to obtain information about a tax authority by providing its unique ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxauthorities/{tax_authority_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            tax_authority_id=tax_authority_unique_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_tax_authority(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization to identify which one the tax authority belongs to."
    ],
    tax_authority_identifier: Annotated[
        str, "The unique identifier for the tax authority to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_tax_authority'."]:
    """Delete a specific tax authority.

    Use this tool to delete a tax authority when you need its removal confirmed."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxauthorities/{tax_authority_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            tax_authority_id=tax_authority_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_tax_exemption(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books for which the tax exemption is being created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_tax_exemption'."]:
    """Create a tax exemption in Zoho Books.

    Use this tool to create a tax exemption in Zoho Books when you need to apply tax exemptions for transactions or customers.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATETAXEXEMPTION_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATETAXEXEMPTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATETAXEXEMPTION_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/taxexemptions".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATETAXEXEMPTION_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_tax_exemptions_list(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization whose tax exemptions are being requested."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_tax_exemptions'."]:
    """Retrieve a list of tax exemptions from Zoho Books.

    Call this tool to get information about the current tax exemptions available in Zoho Books. It provides details on all tax exemptions configured in the system."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxexemptions".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_tax_exemption_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier of the organization for which the tax exemption needs to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    tax_exemption_identifier: Annotated[
        str | None,
        "Unique identifier for the tax exemption to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_tax_exemption'."]:
    """Update the details of a tax exemption.

    This tool updates the information of a specific tax exemption. It should be called when changes to tax exemption records are needed, such as modifying existing details.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATETAXEXEMPTIONDETAILS_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not tax_exemption_identifier:
        missing_params.append(("tax_exemption_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATETAXEXEMPTIONDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATETAXEXEMPTIONDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/settings/taxexemptions/{tax_exemption_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            tax_exemption_id=tax_exemption_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATETAXEXEMPTIONDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_tax_exemption_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to access the organization's tax exemption details.",  # noqa: E501
    ],
    tax_exemption_identifier: Annotated[
        str, "The unique identifier for the tax exemption to retrieve details for."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_tax_exemption'."]:
    """Retrieve the details of a tax exemption using its ID.

    This tool retrieves details about a specific tax exemption identified by its ID. It is useful for understanding exemption specifics within financial settings."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxexemptions/{tax_exemption_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            tax_exemption_id=tax_exemption_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_tax_exemption(
    context: ToolContext,
    organization_id: Annotated[str, "ID of the organization in Zoho Books."],
    tax_exemption_identifier: Annotated[
        str, "Unique identifier of the tax exemption to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_tax_exemption'."]:
    """Delete a specific tax exemption from Zoho Books.

    Use this tool to delete a tax exemption in Zoho Books when you need to remove a specific exemption entry."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/settings/taxexemptions/{tax_exemption_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            tax_exemption_id=tax_exemption_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def log_time_entries(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books. Required for logging time entries.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_time_entries'."]:
    """Log time entries in Zoho Books.

    Use this tool to log time entries for projects in Zoho Books. It should be called when you need to track time spent on project tasks.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["LOGTIMEENTRIES_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["LOGTIMEENTRIES_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["LOGTIMEENTRIES_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/timeentries".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["LOGTIMEENTRIES_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_time_entries(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The unique identifier for the organization. Required to retrieve time entries specific to this organization.",  # noqa: E501
    ],
    start_date_for_time_entries: Annotated[
        str | None,
        "Date from which the time entries should start being fetched. Expected format is YYYY-MM-DD.",  # noqa: E501
    ] = None,
    end_date_for_time_entries: Annotated[
        str | None, "The end date for fetching logged time entries in YYYY-MM-DD format."
    ] = None,
    filter_time_entries_by: Annotated[
        str | None,
        "Filter time entries by date or status. Use values like Date.Today, Date.ThisMonth, Status.Unbilled, etc.",  # noqa: E501
    ] = None,
    project_id: Annotated[
        str | None, "Search for time entries by specifying the project ID."
    ] = None,
    search_time_entries_by_user_id: Annotated[
        str | None,
        "Search and filter time entries based on a specific user's ID. Provide the ID as a string.",
    ] = None,
    sort_time_entries_by: Annotated[
        str | None,
        "Sort time entries by project name, task name, user name, log date, timer start time, or customer name.",  # noqa: E501
    ] = None,
    page_number_to_fetch: Annotated[
        int | None, "Page number to retrieve time entries from, starting at 1 by default."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Defaults to 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_time_entries'."]:
    """Retrieve all time entries with pagination.

    Call this tool to get a list of all time entries for projects, supporting pagination if needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/timeentries".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "from_date": start_date_for_time_entries,
            "to_date": end_date_for_time_entries,
            "filter_by": filter_time_entries_by,
            "project_id": project_id,
            "user_id": search_time_entries_by_user_id,
            "sort_column": sort_time_entries_by,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_time_entries(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "ID of the organization to identify which entity's time entries will be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_time_entries'."]:
    """Delete time tracking entries from projects.

    Use this tool to delete time entries from projects in Zoho Books. It should be called when you need to remove specific time tracking records."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/timeentries".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_time_entry(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization to which the time entry belongs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    time_entry_identifier: Annotated[
        str | None,
        "Unique identifier of the existing time entry to update.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_time_entry'."]:
    """Updates an existing logged time entry.

    Use this tool to modify details of a previously logged time entry in Zoho Books. Ideal when adjustments or updates need to be made to existing time logs.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATETIMEENTRY_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not time_entry_identifier:
        missing_params.append(("time_entry_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATETIMEENTRY_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(REQUEST_BODY_SCHEMAS["UPDATETIMEENTRY_REQUEST_BODY_SCHEMA"], indent=2)
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/projects/timeentries/{time_entry_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            time_entry_id=time_entry_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATETIMEENTRY_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_time_entry_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization for which the time entry details are requested."
    ],
    time_entry_identifier: Annotated[
        str, "Unique identifier of the time entry to retrieve details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_time_entry'."]:
    """Retrieve details of a specific time entry.

    Use this tool to get detailed information about a specific time entry by providing the time entry ID. It's useful for tracking and managing project time entries."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/timeentries/{time_entry_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            time_entry_id=time_entry_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_logged_time_entry(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier of the organization from which the time entry will be deleted."
    ],
    time_entry_identifier: Annotated[
        str, "Unique identifier for the logged time entry to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_time_entry'."]:
    """Delete a specific logged time entry.

    Use this tool to delete a logged time entry from a project by specifying the time entry ID."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/timeentries/{time_entry_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            time_entry_id=time_entry_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def start_time_tracking(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "ID of the organization for which the time tracking is to be started."
    ],
    time_entry_identifier: Annotated[
        str, "Unique identifier for the specific time entry to be tracked."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'start_entry_timer'."]:
    """Initiate time tracking for a specific entry.

    Use this tool to start tracking time for a given time entry in Zoho Books. It should be called when you need to begin or resume tracking time on a project task."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/timeentries/{time_entry_id}/timer/start".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            time_entry_id=time_entry_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def stop_time_tracking(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "ID of the organization where the time entry is being stopped. This ID is required to specify which organization's time tracking should be affected.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'stop_entry_timer'."]:
    """Stop the timer for a time entry.

    Use this tool to stop tracking time for a specific task or activity, such as when taking a break or ending a work session."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/timeentries/timer/stop".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.projects.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_current_running_timer(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization whose running timer is being retrieved."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_running_timer'."]:
    """Retrieve the current running timer for a user.

    Use this tool to get information about the current running timer for personal time tracking in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/projects/timeentries/runningtimer/me".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_organization_user(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization in Zoho Books where the user will be created.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_user'."]:
    """Create a user for your organization in Zoho Books.

    This tool facilitates the creation of a new user within your organization using Zoho Books' services. It should be called when you need to register a new user in your organization's Zoho Books account.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "CREATEORGANIZATIONUSER_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEORGANIZATIONUSER_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEORGANIZATIONUSER_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/users".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEORGANIZATIONUSER_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_organization_users(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization to retrieve its users. Provide a valid organization ID as a string.",  # noqa: E501
    ],
    user_status_filter: Annotated[
        str | None,
        "Filter users based on their status. Options: Status.All, Status.Active, Status.Inactive, Status.Invited, Status.Deleted.",  # noqa: E501
    ] = None,
    sort_users_by_column: Annotated[
        str | None,
        "Specify the attribute to sort users by. Allowed values are name, email, user_role, and status.",  # noqa: E501
    ] = None,
    page_number: Annotated[
        int | None,
        "Page number to be retrieved, with default being 1. Specify to navigate through pages.",
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of user records to retrieve per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_users'."]:
    """Retrieve the list of all users in the organization.

    Use this tool to get an updated list of all users within the organization. Ideal for tasks needing user information or management."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/users".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "filter_by": user_status_filter,
            "sort_column": sort_users_by_column,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_user_details(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization whose user's details are being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    user_identifier: Annotated[
        str | None,
        "Unique identifier of the user to be updated in Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_user'."]:
    """Update user details in Zoho Books.

    Use this tool to update the information of an existing user in Zoho Books. Call it when changes to a user's profile are needed.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEUSERDETAILS_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not user_identifier:
        missing_params.append(("user_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEUSERDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEUSERDETAILS_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/users/{user_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), user_id=user_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEUSERDETAILS_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_user_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books. This is required to specify the organization context for API calls.",  # noqa: E501
    ],
    user_unique_identifier: Annotated[
        str, "A unique string that identifies the user in Zoho Books."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_user'."]:
    """Retrieve detailed information about a specific user in Zoho Books.

    This tool fetches the details of a user from Zoho Books using their user ID. It should be called when user information such as name, email, or role is required."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/users/{user_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), user_id=user_unique_identifier
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def remove_user_from_organization(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier of the organization from which the user will be deleted."
    ],
    user_unique_identifier: Annotated[
        str, "Unique identifier of the user to be deleted from the organization."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_user'."]:
    """Delete a user from the organization.

    This tool removes a specified user from the organization in Zoho Books. It should be called when there is a need to delete a user's access or association with the organization."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/users/{user_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), user_id=user_unique_identifier
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_current_user_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization in Zoho Books to retrieve the current user details.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_current_user'."]:
    """Retrieve details of the current user from Zoho Books.

    Use this tool to get information about the currently authenticated user in Zoho Books. Ideal for scenarios where user-specific data is needed."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/users/me".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_invitation_email(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier of the organization in Zoho Books required to send the invitation.",
    ],
    user_unique_identifier: Annotated[
        str, "Unique identifier of the user to whom the invitation email will be sent."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'invite_user'."]:
    """Send an invitation email to a user in Zoho Books.

    Use this tool to send an invitation email to a specific user by their user ID in Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/users/{user_id}/invite".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), user_id=user_unique_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def activate_inactive_user(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization for which the user will be reactivated. Ensure it matches the organization's records.",  # noqa: E501
    ],
    user_identifier: Annotated[str, "Unique identifier for the user to be activated."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_user_active'."]:
    """Mark an inactive user as active.

    Use this tool to activate a user who is currently marked as inactive in the system."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/users/{user_id}/active".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), user_id=user_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.settings.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def deactivate_user_account(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization within Zoho Books to which the user belongs."
    ],
    user_unique_identifier: Annotated[
        str, "Provide the unique identifier of the user to be deactivated."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_user_inactive'."]:
    """Deactivate a user's account in Zoho Books.

    Use this tool to mark an active user as inactive within Zoho Books. It is useful when needing to revoke access or deactivate an account."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/users/{user_id}/inactive".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), user_id=user_unique_identifier
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_vendor_credit(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the vendor credit is being created. Must be a valid organization ID.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    bill_id: Annotated[
        str | None,
        "Identifier of the bill associated with the vendor credit. Required for linking the credit to a specific transaction.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    ignore_auto_number_generation: Annotated[
        bool | None,
        "Set to true to bypass auto number generation. A vendor credit number becomes mandatory when enabled.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_vendor_credit'."]:
    """Create vendor credit for returns or adjustments.

    Use this tool to record new vendor credits for returned items, overpayments, or adjustments. Supports multi-currency, custom line items, taxes, and workflows.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEVENDORCREDIT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEVENDORCREDIT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEVENDORCREDIT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorcredits".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEVENDORCREDIT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "ignore_auto_number_generation": ignore_auto_number_generation,
            "bill_id": bill_id,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_vendor_credits(
    context: ToolContext,
    organization_id: Annotated[str, "The unique identifier for the organization in Zoho Books."],
    vendor_credit_number_filter: Annotated[
        str | None,
        "Filter vendor credits by specific vendor credit number. Supports partial matching with options like 'startswith' and 'contains'.",  # noqa: E501
    ] = None,
    filter_by_creation_date: Annotated[
        str | None,
        "Filter vendor credits by creation date using yyyy-mm-dd format. Supports date_start, date_end, date_before, and date_after for range filtering.",  # noqa: E501
    ] = None,
    vendor_credit_status: Annotated[
        str | None,
        "Filter vendor credits by their current status. Allowed values: 'open', 'closed', 'void', or 'draft'.",  # noqa: E501
    ] = None,
    total_amount_filter: Annotated[
        str | None,
        "Filter vendor credits by total amount. Use variants: total_start, total_end, total_less_than, total_less_equals, total_greater_than, total_greater_equals.",  # noqa: E501
    ] = None,
    reference_number_filter: Annotated[
        str | None,
        "Filter vendor credits by their reference number, supporting 'startswith' and 'contains' for partial matches.",  # noqa: E501
    ] = None,
    filter_by_vendor_name: Annotated[
        str | None,
        "Filter vendor credits by vendor name, supporting partial matches with 'startswith' and 'contains'.",  # noqa: E501
    ] = None,
    filter_by_item_name: Annotated[
        str | None,
        "Filter vendor credits by item name. Use 'startswith:' or 'contains:' as prefixes for partial matching.",  # noqa: E501
    ] = None,
    item_description_filter: Annotated[
        str | None,
        "Filter vendor credits by item description. Supports partial matching with 'startswith' and 'contains'.",  # noqa: E501
    ] = None,
    filter_by_notes_content: Annotated[
        str | None,
        "Filter vendor credits by notes content. Use partial matching with variants: notes_startswith or notes_contains.",  # noqa: E501
    ] = None,
    filter_by_custom_field: Annotated[
        str | None,
        "Filter vendor credits by custom field values. Use 'custom_field_startswith' or 'custom_field_contains' for partial matching.",  # noqa: E501
    ] = None,
    filter_by_last_modified_time: Annotated[
        str | None,
        "Filter vendor credits by last modified time using ISO 8601 format (yyyy-mm-ddThh:mm:ss-hh:mm).",  # noqa: E501
    ] = None,
    filter_by_customer_id: Annotated[
        int | None,
        "Filter vendor credits by a specific customer ID to find credits associated with that customer. Retrieve customer IDs from the contacts API.",  # noqa: E501
    ] = None,
    filter_by_line_item_id: Annotated[
        int | None,
        "Filter vendor credits by a specific line item ID to find credits containing the item.",
    ] = None,
    filter_by_item_id: Annotated[
        int | None,
        "Filter vendor credits by a specific item ID. Use this to find vendor credits containing the item. Retrieve item IDs from the items API.",  # noqa: E501
    ] = None,
    filter_by_tax_id: Annotated[
        int | None,
        "Filter vendor credits by specific tax ID to find credits with that tax applied. Tax IDs are retrieved from the taxes API.",  # noqa: E501
    ] = None,
    filter_by_status: Annotated[
        str | None,
        "Filter vendor credits by status using predefined values: Status.All, Status.Open, Status.Draft, Status.Closed, Status.Void.",  # noqa: E501
    ] = None,
    search_text: Annotated[
        str | None,
        "Enter text to search vendor credits by credit number, vendor name, and reference number.",
    ] = None,
    sort_by_column: Annotated[
        str | None,
        "Specify which column to sort vendor credits by. Options: vendor_name, vendor_credit_number, balance, total, date, created_time, last_modified_time, reference_number.",  # noqa: E501
    ] = None,
    pagination_page_number: Annotated[
        int | None, "Specify the page number to retrieve results from for pagination. Default is 1."
    ] = 1,
    pagination_records_per_page: Annotated[
        int | None,
        "Specify the number of vendor credit records to return per page. The default value is 200.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_vendor_credits'."]:
    """Retrieve and filter vendor credits from Zoho Books.

    Retrieve a paginated list of vendor credits using various filtering, sorting, and search capabilities, such as by date, status, amount, vendor details, items, taxes, and custom fields."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "vendor_credit_number": vendor_credit_number_filter,
            "date": filter_by_creation_date,
            "status": vendor_credit_status,
            "total": total_amount_filter,
            "reference_number": reference_number_filter,
            "customer_name": filter_by_vendor_name,
            "item_name": filter_by_item_name,
            "item_description": item_description_filter,
            "notes": filter_by_notes_content,
            "custom_field": filter_by_custom_field,
            "last_modified_time": filter_by_last_modified_time,
            "customer_id": filter_by_customer_id,
            "line_item_id": filter_by_line_item_id,
            "item_id": filter_by_item_id,
            "tax_id": filter_by_tax_id,
            "filter_by": filter_by_status,
            "search_text": search_text,
            "sort_column": sort_by_column,
            "page": pagination_page_number,
            "per_page": pagination_records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_vendor_credit(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique identifier of the organization in Zoho Books. Required to update vendor credit.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_credit_id: Annotated[
        str | None,
        "The unique identifier for the vendor credit to be updated. This string is required to locate the specific credit.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_vendor_credit'."]:
    """Update an existing vendor credit in Zoho Books.

    Use this tool to update the details of a specific vendor credit in Zoho Books. Call this when modifications to vendor credits are needed.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEVENDORCREDIT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not vendor_credit_id:
        missing_params.append(("vendor_credit_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORCREDIT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORCREDIT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), vendor_credit_id=vendor_credit_id
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEVENDORCREDIT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_vendor_credit_details(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization whose vendor credit details are being requested.",  # noqa: E501
    ],
    vendor_credit_id: Annotated[
        str, "Unique identifier for the vendor credit to retrieve details."
    ],
    output_format: Annotated[
        str | None,
        "Specify the format for vendor credit details. Options: json, xml, csv, xls, pdf, html, jhtml. Default is html.",  # noqa: E501
    ] = None,
    export_vendor_credit_pdf: Annotated[
        bool | None,
        "Set to true to export the vendor credit as a PDF with default print options. Accepts 'true', 'false', 'on', 'off'.",  # noqa: E501
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_vendor_credit'."]:
    """Retrieve details of a specific vendor credit.

    Use this tool to obtain detailed information about a vendor credit by providing the specific vendor credit ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), vendor_credit_id=vendor_credit_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "print": export_vendor_credit_pdf,
            "accept": output_format,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_vendor_credit(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to specify which organization's vendor credit to delete.",  # noqa: E501
    ],
    vendor_credit_identifier: Annotated[
        str, "Unique identifier of the vendor credit to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_vendor_credit'."]:
    """Delete a vendor credit by its ID.

    Use this tool to delete a specific vendor credit using its unique ID. This is typically required when a vendor credit is no longer valid or was added by mistake."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def open_vendor_credit_status(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization in Zoho Books whose vendor credit status needs to be marked as open.",  # noqa: E501
    ],
    vendor_credit_identifier: Annotated[
        str, "Unique identifier for the vendor credit to be marked as open."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_vendor_credit_open'."]:
    """Change a vendor credit status to open in Zoho Books.

    Use this tool to update the status of an existing vendor credit to 'open' in Zoho Books. This is useful when you need to reactivate a vendor credit for further processing or adjustments."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/status/open".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def mark_vendor_credit_void(
    context: ToolContext,
    organization_id: Annotated[str, "Provide the unique ID of the organization in Zoho Books."],
    vendor_credit_identifier: Annotated[
        str, "Unique identifier for the vendor credit to be marked as void."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'mark_vendor_credit_void'."]:
    """Mark an existing vendor credit as void in Zoho Books.

    Use this tool to mark a specific vendor credit as void. Typically called when you need to invalidate a previously issued vendor credit in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/status/void".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def submit_vendor_credit_for_approval(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization where the vendor credit is submitted for approval."
    ],
    vendor_credit_unique_id: Annotated[
        str, "Unique identifier of the vendor credit to be submitted for approval."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'submit_vendor_credit'."]:
    """Submit a vendor credit for approval.

    This tool submits a specified vendor credit for approval based on its unique ID. Use it to move vendor credits from draft to approval status in Zoho Books."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/submit".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_unique_id,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def approve_vendor_credit(
    context: ToolContext,
    organization_identifier: Annotated[
        str,
        "The ID of the organization in Zoho Books. This uniquely identifies the organization for which the vendor credit approval will be processed.",  # noqa: E501
    ],
    vendor_credit_identifier: Annotated[
        str, "Unique identifier for the vendor credit to be approved."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'approve_vendor_credit'."]:
    """Approve a vendor credit in Zoho Books.

    This tool approves a vendor credit in Zoho Books, marking the credit as accepted for the specified vendor credit ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/approve".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
        ),
        method="POST",
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_bills_with_vendor_credit(
    context: ToolContext,
    organization_id: Annotated[
        str, "ID of the organization to fetch applicable bills for vendor credit."
    ],
    vendor_credit_id: Annotated[
        str, "Unique identifier for the vendor credit to list the applied bills."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_bills_credited'."]:
    """List bills with applied vendor credit from a vendor credit ID.

    Fetch a list of bills where a specific vendor credit has been applied. Use this tool to understand how vendor credits are distributed across bills."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/bills".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), vendor_credit_id=vendor_credit_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def apply_vendor_credit_to_bill(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization where the vendor credit will be applied. Required for identification within Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_credit_identifier: Annotated[
        str | None,
        "Unique identifier for the vendor credit to be applied to a bill.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'apply_credits_to_a_bill'."]:
    """Apply vendor credit to an existing bill in Zoho Books.

    Use this tool to apply a specific vendor credit to existing bills. It helps manage and track accounts payable by adjusting bills with vendor credits.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "APPLYVENDORCREDITTOBILL_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not vendor_credit_identifier:
        missing_params.append(("vendor_credit_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["APPLYVENDORCREDITTOBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["APPLYVENDORCREDITTOBILL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/bills".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["APPLYVENDORCREDITTOBILL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def remove_vendor_bill_credit(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique ID of the organization for which the vendor credit bill is to be deleted."
    ],
    vendor_credit_identifier: Annotated[
        str,
        "Unique identifier of the vendor credit to be deleted. Required for bill credit removal.",
    ],
    vendor_credit_bill_identifier: Annotated[
        str, "Unique identifier of the vendor credit bill to delete the applied credits."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_vendor_credit_bill'."]:
    """Delete credits applied to a vendor bill.

    Use this tool to remove credits applied to a specific vendor bill using the vendor_credit_id and vendor_credit_bill_id."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/bills/{vendor_credit_bill_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
            vendor_credit_bill_id=vendor_credit_bill_identifier,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def refund_vendor_credit(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The ID of the organization for which the vendor credit refund is being processed.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_credit_identifier: Annotated[
        str | None,
        "Unique identifier for the vendor credit that needs to be refunded.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'refund_vendor_credit'."]:
    """Process a refund for vendor credit.

    Use this tool to initiate a refund for a specified vendor credit. Suitable when needing to refund amounts credited to a vendor.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["REFUNDVENDORCREDIT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not vendor_credit_identifier:
        missing_params.append(("vendor_credit_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["REFUNDVENDORCREDIT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["REFUNDVENDORCREDIT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["REFUNDVENDORCREDIT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_vendor_credit_refunds(
    context: ToolContext,
    organization_identifier: Annotated[
        str, "Unique string ID of the organization for which refunds are to be listed."
    ],
    vendor_credit_id: Annotated[
        str, "The unique identifier for a specific vendor credit whose refunds are to be listed."
    ],
    page_number: Annotated[
        int | None,
        "Page number for pagination, specifying which set of results to retrieve. Default is 1.",
    ] = 1,
    records_per_page: Annotated[
        int | None,
        "Specify the number of refunds to return per page for pagination. Default is 200.",
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_vendor_credit_refunds'."]:
    """Retrieve all refunds for a specified vendor credit.

    Use this tool to get a list of all refunds associated with a particular vendor credit by providing the vendor credit ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), vendor_credit_id=vendor_credit_id
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_identifier,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_vendor_credit_refund(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Unique identifier for the organization. This is needed to specify which organization the vendor credit refund update applies to.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_credit_identifier: Annotated[
        str | None,
        "Unique identifier of the vendor credit to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_credit_refund_identifier: Annotated[
        str | None,
        "Unique identifier for the vendor credit refund transaction that needs to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_vendor_credit_refund'."]:
    """Update a refunded vendor credit transaction.

    Use this tool to update details of a refunded vendor credit transaction by specifying the vendor credit ID and refund ID.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEVENDORCREDITREFUND_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not vendor_credit_identifier:
        missing_params.append(("vendor_credit_identifier", "path"))
    if not vendor_credit_refund_identifier:
        missing_params.append(("vendor_credit_refund_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORCREDITREFUND_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORCREDITREFUND_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/refunds/{vendor_credit_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
            vendor_credit_refund_id=vendor_credit_refund_identifier,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEVENDORCREDITREFUND_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_vendor_credit_refund(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization to which the vendor credit belongs."
    ],
    vendor_credit_identifier: Annotated[
        str, "Unique identifier for the vendor credit to retrieve the refund details."
    ],
    vendor_credit_refund_id: Annotated[
        str,
        "Unique identifier of the vendor credit refund for the specific transaction. Required to retrieve refund details.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_vendor_credit_refund'."]:
    """Retrieve a refund for a specific vendor credit.

    Fetches details of a refund associated with a specific vendor credit using the vendor credit ID and refund ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/refunds/{vendor_credit_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
            vendor_credit_refund_id=vendor_credit_refund_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_vendor_credit_refund(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization to which the vendor credit refund belongs."
    ],
    vendor_credit_identifier: Annotated[
        str, "Unique identifier for the vendor credit, required to delete the refund."
    ],
    vendor_credit_refund_id: Annotated[
        str, "Unique identifier for the specific vendor credit refund you wish to delete."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_vendor_credit_refund'."]:
    """Delete a vendor credit refund in Zoho Books.

    Use this tool to delete a specific vendor credit refund in Zoho Books when you have the vendor credit ID and refund ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/refunds/{vendor_credit_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
            vendor_credit_refund_id=vendor_credit_refund_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def fetch_vendor_credit_refunds(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier of the organization for which to list vendor credit refunds."
    ],
    search_vendor_credits_by_customer_id: Annotated[
        int | None, "Search for vendor credits linked to a specific customer using their ID."
    ] = None,
    vendor_credit_last_modified_time: Annotated[
        str | None,
        "Search vendor credits using the last modified time as a filter. This expects a date-time string, typically in ISO 8601 format, to narrow down results by when they were last modified.",  # noqa: E501
    ] = None,
    sort_vendor_credits_by_column: Annotated[
        str | None,
        "Specify the column to sort vendor credits by. Allowed values: vendor_name, vendor_credit_number, balance, total, date, created_time, last_modified_time, reference_number.",  # noqa: E501
    ] = None,
    pagination_page_number: Annotated[
        int | None, "Specify the page number of results to retrieve for pagination. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of vendor credits to return per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_vendor_credit_refunds1'."]:
    """Retrieve a paginated list of vendor credit refunds.

    Use this tool to obtain detailed information about vendor credit refunds, useful for financial reconciliation and tracking vendor transactions. This tool supports pagination."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "customer_id": search_vendor_credits_by_customer_id,
            "last_modified_time": vendor_credit_last_modified_time,
            "sort_column": sort_vendor_credits_by_column,
            "page": pagination_page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def add_vendor_credit_comment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization. Required to specify which organization the vendor credit belongs to.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_credit_identifier: Annotated[
        str | None,
        "Unique identifier for the vendor credit to which the comment will be added.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'add_vendor_credit_comment'."]:
    """Add a comment to an existing vendor credit.

    Use this tool to add a comment to a vendor credit in Zoho Books. It should be called when you need to attach notes or additional information to a vendor credit entry.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "ADDVENDORCREDITCOMMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not vendor_credit_identifier:
        missing_params.append(("vendor_credit_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDVENDORCREDITCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["ADDVENDORCREDITCOMMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["ADDVENDORCREDITCOMMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_vendor_credit_comments(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization. This ID is required to access the vendor credit comments.",  # noqa: E501
    ],
    vendor_credit_identifier: Annotated[
        str,
        "The unique identifier for the specific vendor credit to retrieve its history and comments.",  # noqa: E501
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_vendor_credit_comments'."]:
    """Retrieve history and comments for a vendor credit.

    Use this tool to get the history and comments associated with a specific vendor credit by providing the vendor credit ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/comments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_identifier,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.debitnotes.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_vendor_credit_comment(
    context: ToolContext,
    organization_id: Annotated[str, "The unique ID of the organization in Zoho Books."],
    vendor_credit_id: Annotated[
        str, "Unique identifier of the vendor credit to specify which comment to delete."
    ],
    comment_id: Annotated[str, "Unique identifier of the vendor credit comment to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_vendor_credit_comment'."]:
    """Delete a vendor credit comment in Zoho Books.

    Use this tool to delete a specific comment from a vendor credit in Zoho Books. This is useful when you need to manage or clean up vendor credit records by removing unnecessary or incorrect comments."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorcredits/{vendor_credit_id}/comments/{comment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            vendor_credit_id=vendor_credit_id,
            comment_id=comment_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def create_vendor_payment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID of the organization to which the vendor payment belongs.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'create_vendor_payment'."]:
    """Create and apply a payment to a vendor's bill.

    This tool is used to record a payment made to a vendor, with the option to apply the payment either partially or fully to the vendor's outstanding bills.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["CREATEVENDORPAYMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEVENDORPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["CREATEVENDORPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["CREATEVENDORPAYMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_vendor_payments(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier of the organization whose vendor payments you want to list."
    ],
    vendor_name_query: Annotated[
        str | None, "Search payments by vendor name using parameters like startswith or contains."
    ] = None,
    search_by_reference_number: Annotated[
        str | None,
        "Search payments using the reference number. Supports variants: 'startswith' and 'contains'.",  # noqa: E501
    ] = None,
    payment_number_search: Annotated[
        str | None,
        "Search payments using the payment number with options for exact match, starts with, or contains.",  # noqa: E501
    ] = None,
    payment_date_filter: Annotated[
        str | None,
        "Specify the date for payment filtering. Use variants like 'date_start', 'date_end', 'date_before', and 'date_after'.",  # noqa: E501
    ] = None,
    payment_amount_filter: Annotated[
        float | None,
        "Filter payments by amount paid to the vendor. Use variants: 'less_than', 'less_equals', 'greater_than', 'greater_equals' to specify the condition.",  # noqa: E501
    ] = None,
    search_by_payment_mode: Annotated[
        str | None,
        "Search payments by payment mode using variants like 'startswith' or 'contains'.",
    ] = None,
    search_with_payment_notes: Annotated[
        str | None, "Search payments using notes with options like startswith or contains."
    ] = None,
    vendor_id: Annotated[
        str | None, "The unique ID of the vendor, used to search payments by vendor ID."
    ] = None,
    last_modified_time_filter: Annotated[
        str | None,
        "Filter vendor payments by their last modified time. Use a date-time string in ISO 8601 format.",  # noqa: E501
    ] = None,
    search_payments_by_bill_id: Annotated[
        str | None, "Search payments using the specific Bill ID associated with the transaction."
    ] = None,
    search_by_description: Annotated[
        str | None,
        "Search payments by description. Use 'description_startswith' or 'description_contains' variants for specific matches.",  # noqa: E501
    ] = None,
    filter_payment_mode: Annotated[
        str | None,
        "Filter payments by payment mode. Options include All, Check, Cash, BankTransfer, Paypal, CreditCard, GoogleCheckout, Credit, Authorizenet, BankRemittance, Payflowpro, and Others.",  # noqa: E501
    ] = None,
    search_text: Annotated[
        str | None,
        "Search for payments using reference number, vendor name, or payment description.",
    ] = None,
    sort_payments_by: Annotated[
        str | None,
        "Sort payments by column. Options: vendor_name, date, reference_number, amount, balance.",
    ] = None,
    page_number_to_fetch: Annotated[
        int | None, "Specify the page number of results to fetch. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Specify the number of records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_vendor_payments'."]:
    """Fetch all payments made to vendors.

    Use this tool to retrieve a list of all vendor payments. It helps manage and track financial transactions with vendors."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "vendor_name": vendor_name_query,
            "reference_number": search_by_reference_number,
            "payment_number": payment_number_search,
            "date": payment_date_filter,
            "amount": payment_amount_filter,
            "payment_mode": search_by_payment_mode,
            "notes": search_with_payment_notes,
            "vendor_id": vendor_id,
            "last_modified_time": last_modified_time_filter,
            "bill_id": search_payments_by_bill_id,
            "description": search_by_description,
            "filter_by": filter_payment_mode,
            "search_text": search_text,
            "sort_column": sort_payments_by,
            "page": page_number_to_fetch,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_vendor_payment_with_custom_id(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization within Zoho Books.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_field_api_name: Annotated[
        str | None,
        "The API name of the unique custom field used to identify the vendor payment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    custom_field_unique_value: Annotated[
        str | None,
        "The unique value of the custom field used to identify or create a vendor payment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    create_new_record_if_not_exists: Annotated[
        bool | None,
        "Set to true to create a new vendor payment if no existing record matches the unique custom field value.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'update_vendor_payment_using_custom_field'."
]:
    """Update or create a vendor payment using a unique custom field.

    This tool updates an existing vendor payment by using a unique custom field as an identifier. If the unique value is not found and the X-Upsert option is enabled, it will create a new vendor payment if sufficient details are provided.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEVENDORPAYMENTWITHCUSTOMID_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENTWITHCUSTOMID_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENTWITHCUSTOMID_REQUEST_BODY_SCHEMA"],
                    indent=2,
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENTWITHCUSTOMID_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "X-Unique-Identifier-Key": custom_field_api_name,
            "X-Unique-Identifier-Value": custom_field_unique_value,
            "X-Upsert": create_new_record_if_not_exists,
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_multiple_vendor_payments(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization from which the vendor payments are to be deleted.",  # noqa: E501
    ],
    vendor_payment_ids: Annotated[str, "Comma-separated list of vendor payment IDs to delete."],
    bulk_delete: Annotated[bool, "Set to true to perform bulk deletion of vendor payments."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'bulk_delete_vendor_payments'."]:
    """Delete multiple vendor payments in one action.

    Use this tool to delete multiple vendor payments at once, streamlining payment management and cleanup tasks."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorpayments".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL")
        ),
        method="DELETE",
        params=remove_none_values({
            "organization_id": organization_id,
            "vendorpayment_id": vendor_payment_ids,
            "bulk_delete": bulk_delete,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_vendor_payment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "Provide the specific ID of the organization for which the vendor payment is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    payment_identifier: Annotated[
        str | None,
        "The unique identifier of the vendor payment to be updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_vendor_payment'."]:
    """Update or modify an existing vendor payment.

    Use this tool to update an existing vendor payment, including modifying the amount applied to bills.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENT_REQUEST_BODY_SCHEMA"],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not payment_identifier:
        missing_params.append(("payment_identifier", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=payment_identifier
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def fetch_vendor_payment_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "The unique identifier for the organization to retrieve the vendor payment details."
    ],
    payment_identifier: Annotated[
        str, "Unique identifier for the payment to retrieve its details."
    ],
    include_tax_information: Annotated[
        bool | None, "Set to true to fetch tax information for the vendor payment."
    ] = None,
    fetch_statement_line_info: Annotated[
        bool | None, "Set to true to fetch statement line information for the vendor payment."
    ] = None,
    print_payment: Annotated[
        bool | None, "Specify true to print the Vendor Payment details."
    ] = None,
    is_bill_payment_id: Annotated[
        bool | None, "True if the ID is for a Bill Payment, false for a Vendor Payment."
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_vendor_payment'."]:
    """Retrieve details of a vendor payment by payment ID.

    Call this tool to get detailed information about a specific vendor payment using the payment ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=payment_identifier
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "fetchTaxInfo": include_tax_information,
            "fetchstatementlineinfo": fetch_statement_line_info,
            "print": print_payment,
            "is_bill_payment_id": is_bill_payment_id,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_vendor_payment(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique ID of the organization for which the vendor payment is to be deleted. This is required to identify the correct organization within Zoho Books.",  # noqa: E501
    ],
    vendor_payment_id: Annotated[str, "Unique identifier of the vendor payment to be deleted."],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_vendor_payment'."]:
    """Delete an existing vendor payment in Zoho Books.

    Use this tool to delete a specific vendor payment in Zoho Books when you have the payment ID and need to erase it from the records."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=vendor_payment_id
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def list_vendor_payment_refunds(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "ID of the organization in Zoho Books to list the refunds for. This should be a unique identifier as specified by Zoho Books.",  # noqa: E501
    ],
    payment_identifier: Annotated[
        str, "Unique identifier of the vendor payment to fetch refunds for."
    ],
    page_number: Annotated[
        int | None, "Page number to be fetched, starting from 1. Default is 1."
    ] = 1,
    records_per_page: Annotated[
        int | None, "Number of records to fetch per page. Default is 200."
    ] = 200,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'list_vendor_payment_refunds'."]:
    """List all refunds for a vendor payment.

    Retrieve a list of all refunds associated with a specific vendor payment in Zoho Books."""
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=payment_identifier
        ),
        method="GET",
        params=remove_none_values({
            "organization_id": organization_id,
            "page": page_number,
            "per_page": records_per_page,
        }),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def refund_vendor_overpayment(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_identifier: Annotated[
        str | None,
        "The unique ID representing the organization. Required to refund vendor overpayment.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_payment_id: Annotated[
        str | None,
        "Unique identifier for the vendor payment to be refunded.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'refund_excess_vendor_payment'."]:
    """Refund excess amount paid to a vendor.

    Use this tool to process a refund for any excess amount that has been paid to a vendor. This should be called when an overpayment needs to be returned to the vendor.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "REFUNDVENDOROVERPAYMENT_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_identifier:
        missing_params.append(("organization_identifier", "query"))
    if not vendor_payment_id:
        missing_params.append(("vendor_payment_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["REFUNDVENDOROVERPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["REFUNDVENDOROVERPAYMENT_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}/refunds".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=vendor_payment_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["REFUNDVENDOROVERPAYMENT_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_identifier}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_vendor_payment_refund_details(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization whose vendor payment refund details are requested."
    ],
    payment_identifier: Annotated[
        str, "Unique identifier for the payment associated with the vendor refund."
    ],
    vendor_payment_refund_id: Annotated[
        str, "Unique identifier for the vendor payment refund to retrieve its details."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'get_vendor_payment_refund'."]:
    """Retrieve details of a specific vendor payment refund.

    Use this tool to obtain information about a particular refund of a vendor payment, identified by the payment ID and refund ID."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}/refunds/{vendorpayment_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            payment_id=payment_identifier,
            vendorpayment_refund_id=vendor_payment_refund_id,
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.UPDATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def update_vendor_payment_refund(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "The unique identifier for the organization associated with the refund transaction.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    payment_identifier: Annotated[
        str | None,
        "Unique identifier of the payment. Required to specify which payment is being updated.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_payment_refund_id: Annotated[
        str | None,
        "Unique identifier of the vendor payment refund required for updating the transaction.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'update_vendor_payment_refund'."]:
    """Update the refunded transaction for a vendor payment.

    Call this tool to update details related to a vendor payment refund. It is used when modifications to an existing refund transaction are necessary.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "UPDATEVENDORPAYMENTREFUND_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not payment_identifier:
        missing_params.append(("payment_identifier", "path"))
    if not vendor_payment_refund_id:
        missing_params.append(("vendor_payment_refund_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENTREFUND_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENTREFUND_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}/refunds/{vendorpayment_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            payment_id=payment_identifier,
            vendorpayment_refund_id=vendor_payment_refund_id,
        ),
        method="PUT",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["UPDATEVENDORPAYMENTREFUND_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.DELETE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def delete_vendor_payment_refund(
    context: ToolContext,
    organization_id: Annotated[
        str, "The ID of the organization to which the vendor payment refund belongs."
    ],
    payment_identifier: Annotated[str, "Unique identifier of the payment to be deleted."],
    vendor_payment_refund_id: Annotated[
        str, "Unique identifier of the vendor payment refund to be deleted."
    ],
) -> Annotated[dict[str, Any], "Response from the API endpoint 'delete_vendor_payment_refund'."]:
    """Delete a refund from an existing vendor payment.

    Use this tool to remove a specific refund associated with a vendor payment. It's useful when a refund needs to be retracted or corrected."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}/refunds/{vendorpayment_refund_id}".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"),
            payment_id=payment_identifier,
            vendorpayment_refund_id=vendor_payment_refund_id,
        ),
        method="DELETE",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.CREATE"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def send_vendor_payment_email(
    context: ToolContext,
    mode: Annotated[
        ToolMode,
        "Operation mode: 'get_request_schema' returns the OpenAPI spec "
        "for the request body, 'execute' performs the actual operation",
    ],
    organization_id: Annotated[
        str | None,
        "ID of the organization for which the vendor payment email is being sent.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    vendor_payment_id: Annotated[
        str | None,
        "Unique identifier for the vendor payment. Used to retrieve and send the corresponding payment receipt via email.  Required when mode is 'execute', ignored when mode is 'get_request_schema'.",  # noqa: E501
    ] = None,
    email_attachments: Annotated[
        str | None,
        "List of file paths or URLs to attach to the email.  Only used when mode is 'execute'.",
    ] = None,
    attached_file_name: Annotated[
        str | None,
        "Specify the name of the file to be attached to the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    send_vendor_payment_attachment: Annotated[
        bool | None,
        "Set to true to include the vendor payment attachment in the email.  Only used when mode is 'execute'.",  # noqa: E501
    ] = None,
    request_body: Annotated[
        str | None,
        "Stringified JSON representing the request body. Required when "
        "mode is 'execute', ignored when mode is 'get_request_schema'",
    ] = None,
) -> Annotated[dict[str, Any], "Response from the API endpoint 'email_vendor_payment'."]:
    """Send a payment receipt email to a vendor.

    This tool sends a vendor payment receipt to a vendor via email. You can customize the email content, attach files, and control sender preferences. If no customizations are provided, the email will use default templates.

    Note: Understanding the request schema is necessary to properly create
    the stringified JSON input object for execution.\n\nThis operation also requires path, query parameters.

    Modes:
    - GET_REQUEST_SCHEMA: Returns the schema. Only call if you don't
      already have it. Do NOT call repeatedly if you already received
      the schema.
    - EXECUTE: Performs the operation with the provided request body
      JSON.\n      Note: You must also provide the required path, query parameters when executing.

    If you need the schema, call with mode='get_request_schema' ONCE, then execute.
    """  # noqa: E501
    if mode == ToolMode.GET_REQUEST_SCHEMA:
        return {
            "request_body_schema": REQUEST_BODY_SCHEMAS[
                "SENDVENDORPAYMENTEMAIL_REQUEST_BODY_SCHEMA"
            ],
            "instructions": (
                "Use the request_body_schema to construct a valid JSON object. "
                "Once you have populated the object following the schema "
                "structure and requirements, call this tool again with "
                "mode='execute' and the stringified JSON as the "
                "request_body parameter along with the required path, query parameters. "
                "Do NOT call the schema mode again - you already have "
                "the schema now."
            ),
        }

    # Mode is EXECUTE - validate parameters
    # Validate required parameters
    missing_params = []
    if not organization_id:
        missing_params.append(("organization_id", "query"))
    if not vendor_payment_id:
        missing_params.append(("vendor_payment_id", "path"))

    if missing_params:
        raise RetryableToolError(
            message="Missing required parameters: {param_names}",
            developer_message=("Required parameters validation failed: {param_details}"),
            additional_prompt_content=(
                "The following required parameters are missing: "
                "{param_details}. Please call this tool again with all "
                "required parameters."
            ),
        )

    # Validate request body is provided (not None or empty string)
    # Note: Empty objects like {} are allowed - schema validation will check if valid
    if request_body is None or request_body.strip() == "":
        raise RetryableToolError(
            message="Request body is required when mode is 'execute'",
            developer_message="The request_body parameter was null or empty string",
            additional_prompt_content=(
                "The request body is required to perform this operation. "
                "Use the schema below to construct a valid JSON object, "
                "then call this tool again in execute mode with the "
                "stringified JSON as the request_body parameter.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDVENDORPAYMENTEMAIL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        )

    # Parse JSON
    try:
        request_data = json.loads(request_body)
    except json.JSONDecodeError as e:
        raise RetryableToolError(
            message=f"Invalid JSON in request body: {e!s}",
            developer_message=f"JSON parsing failed: {e!s}",
            additional_prompt_content=(
                f"The request body contains invalid JSON. Error: {e!s}\n\n"
                "Please provide a valid JSON string that matches the schema "
                "below, then call this tool again in execute mode.\n\n"
                "Schema:\n\n"
                + json.dumps(
                    REQUEST_BODY_SCHEMAS["SENDVENDORPAYMENTEMAIL_REQUEST_BODY_SCHEMA"], indent=2
                )
            ),
        ) from e

    response = await make_request_with_schema_validation(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=vendor_payment_id
        ),
        method="POST",
        request_data=request_data,
        schema=REQUEST_BODY_SCHEMAS["SENDVENDORPAYMENTEMAIL_REQUEST_BODY_SCHEMA"],
        params=remove_none_values({
            "organization_id": organization_id,
            "send_attachment": send_vendor_payment_attachment,
            "attachments": email_attachments,
            "file_name": attached_file_name,
        }),
        headers=remove_none_values({
            "Content-Type": "application/json",
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            ),
        }),
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}


@tool(
    requires_auth=OAuth2(id="arcade-zoho", scopes=["ZohoBooks.vendorpayments.READ"]),
    requires_secrets=["ZOHO_SERVER_URL"],
)
async def get_vendor_payment_email_content(
    context: ToolContext,
    organization_id: Annotated[
        str,
        "The unique identifier for the organization. Required to retrieve the vendor payment email content.",  # noqa: E501
    ],
    vendor_payment_id: Annotated[
        str, "Unique identifier for the vendor payment to retrieve email content."
    ],
) -> Annotated[
    dict[str, Any], "Response from the API endpoint 'get_vendor_payment_email_content'."
]:
    """Retrieve email content for a vendor payment receipt.

    This tool retrieves the pre-filled email details for a vendor payment, including the message subject, body, recipients, sender, and attachments. Use it to easily compose and send a payment receipt email to vendors."""  # noqa: E501
    request_data = remove_none_values({})
    content = json.dumps(request_data) if request_data else None
    response = await make_request(
        url="{zoho_server_url}/books/v3/vendorpayments/{payment_id}/email".format(
            zoho_server_url=context.get_secret("ZOHO_SERVER_URL"), payment_id=vendor_payment_id
        ),
        method="GET",
        params=remove_none_values({"organization_id": organization_id}),
        headers=remove_none_values({
            "Authorization": "Bearer {authorization}".format(  # noqa: UP032
                authorization=context.get_auth_token_or_empty()
            )
        }),
        content=content,
    )
    try:
        return {"response_json": response.json()}
    except Exception:
        return {"response_text": response.text}
