From daa683d7c5573d907c2cd5b2976f4e20a925593c Mon Sep 17 00:00:00 2001 From: shrir-j Date: Sat, 8 Aug 2026 12:13:34 +0530 Subject: [PATCH 1/9] Add next invoice number generator tool --- .../shared/invoices/parameters.py | 4 ++++ .../shared/invoices/prompts.py | 6 ++++++ .../shared/invoices/tool_handlers.py | 11 +++++++++++ python/paypal_agent_toolkit/shared/tools.py | 11 +++++++++++ typescript/src/shared/api.ts | 3 +++ typescript/src/shared/functions.ts | 17 +++++++++++++++++ typescript/src/shared/parameters.ts | 2 ++ typescript/src/shared/prompts.ts | 6 ++++++ typescript/src/shared/tools.ts | 13 +++++++++++++ 9 files changed, 73 insertions(+) diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 7c369826..35230c88 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -199,6 +199,10 @@ class GenerateInvoiceQrCodeParameters(BaseModel): height: int = Field(300, description="The QR code height") +class GenerateInvoiceNumberParameters(BaseModel): + pass + + class ReminderInterval(BaseModel): unit: Literal["DAY"] = Field("DAY", description="The unit of time for the reminder interval. The interval unit is always DAY.") value: int = Field(..., description="The number of interval units before/after the due date at which to send the reminder.") diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index f0a62b88..57419388 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -58,6 +58,12 @@ This function generates a QR code for an invoice, which can be used to pay the invoice using a mobile device or scanning app. """ +GENERATE_INVOICE_NUMBER_PROMPT = """ +Generate the next invoice number available to the merchant. + +This function generates the next invoice number by using the prefix and suffix from the merchant's last invoice number and incrementing the numeric portion by one (e.g. INVOICE-1234 -> INVOICE-1235). +""" + SETUP_INVOICE_AUTO_REMINDER_PROMPT = """ Initialize the invoice auto reminder configuration for the merchant's PayPal account. diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index 624def9d..ba7ea4c9 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -145,6 +145,17 @@ def generate_invoice_qrcode(client, params: dict): return json.dumps(response) +def generate_invoice_number(client, params: dict): + + GenerateInvoiceNumberParameters(**params) + payload = {"fetch_id": False} + + url = "/v2/invoicing/generate-next-invoice-number" + response = client.post(uri=url, payload=payload) + + return json.dumps(response) + + def setup_invoice_auto_reminders(client, params: dict): validated = SetupInvoiceAutoReminderParameters(**params) diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index 0b5aea90..424229a3 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -25,6 +25,7 @@ SEND_INVOICE_REMINDER_PROMPT, CANCEL_SENT_INVOICE_PROMPT, GENERATE_INVOICE_QRCODE_PROMPT, + GENERATE_INVOICE_NUMBER_PROMPT, SETUP_INVOICE_AUTO_REMINDER_PROMPT, UPDATE_INVOICE_AUTO_REMINDER_PROMPT, ) @@ -78,6 +79,7 @@ SendInvoiceReminderParameters, CancelSentInvoiceParameters, GenerateInvoiceQrCodeParameters, + GenerateInvoiceNumberParameters, SetupInvoiceAutoReminderParameters, UpdateInvoiceAutoReminderParameters, ) @@ -130,6 +132,7 @@ send_invoice_reminder, cancel_sent_invoice, generate_invoice_qrcode, + generate_invoice_number, setup_invoice_auto_reminders, update_invoice_auto_reminder, ) @@ -326,6 +329,14 @@ "actions": {"invoices": {"generateQRC": True}}, "execute": generate_invoice_qrcode, }, + { + "method": "generate_invoice_number", + "name": "Generate Invoice Number", + "description": GENERATE_INVOICE_NUMBER_PROMPT.strip(), + "args_schema": GenerateInvoiceNumberParameters, + "actions": {"invoices": {"generateInvoiceNumber": True}}, + "execute": generate_invoice_number, + }, { "method": "setup_invoice_auto_reminders", "name": "Setup Invoice Auto Reminders", diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index c59bb0eb..0560b9db 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -17,6 +17,7 @@ import { getShipmentTracking, updateShipmentTracking, generateInvoiceQrCode, + generateInvoiceNumber, createOrder, getOrder, listDisputes, @@ -111,6 +112,8 @@ class PayPalAPI { return updateInvoiceAutoReminder(this.paypalClient, this.context, arg); case 'generate_invoice_qr_code': return generateInvoiceQrCode(this.paypalClient, this.context, arg); + case 'generate_invoice_number': + return generateInvoiceNumber(this.paypalClient, this.context, arg); case 'create_product': return createProduct(this.paypalClient, this.context, arg); case 'list_products': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index e968814e..ba8b11cd 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -8,6 +8,7 @@ import { activateRecurringSeriesParameters, createOrderParameters, generateInvoiceQrCodeParameters, + generateInvoiceNumberParameters, getOrderParameters, listInvoicesParameters, sendInvoiceParameters, @@ -386,6 +387,22 @@ export async function generateInvoiceQrCode( } } +export async function generateInvoiceNumber( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + const url = `${client.getBaseUrl()}/v2/invoicing/generate-next-invoice-number`; + const headers = await client.getHeaders(); + try { + const response = await axios.post(url, { fetch_id: false }, { headers }); + return response.data; + } catch (error: any) { + logger('[generateInvoiceNumber] Error generating invoice number:', error.message); + handleAxiosError(error); + } +} + // === PRODUCT FUNCTIONS === export async function createProduct( client: PayPalClient, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index cffef739..e726f22e 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -206,6 +206,8 @@ export const generateInvoiceQrCodeParameters = (context: Context) => z.object({ height: z.number().default(300).describe("The QR code height") }).describe("generate invoice qr code request payload"); +export const generateInvoiceNumberParameters = (context: Context) => z.object({}).describe("generate next invoice number request payload"); + const invoiceReminderConfiguration = z.object({ type: z.enum(['BEFORE_DUE', 'AFTER_DUE']).describe('The type of reminder. BEFORE_DUE sends a reminder before the invoice due date; AFTER_DUE sends a reminder after the invoice due date.'), interval: z.object({ diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index 23fdb2a4..61e89329 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -74,6 +74,12 @@ Generate a QR code for an invoice. This function generates a QR code for an invoice, which can be used to pay the invoice using a mobile device or scanning app. `; +export const generateInvoiceNumberPrompt = (context: Context) => ` +Generate the next invoice number available to the merchant. + +This function generates the next invoice number by using the prefix and suffix from the merchant's last invoice number and incrementing the numeric portion by one (e.g. INVOICE-1234 -> INVOICE-1235). +`; + export const createProductPrompt = (context: Context) => ` Create a product in PayPal using product catalog - create products API. This function creates a new product that will be used in subscription plans, subscriptions. diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index c7b1fee7..71dde849 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -14,6 +14,7 @@ import { createShipmentPrompt, getShipmentTrackingPrompt, generateInvoiceQrCodePrompt, + generateInvoiceNumberPrompt, createOrderPrompt, getOrderPrompt, updateShipmentTrackingPrompt, @@ -53,6 +54,7 @@ import { createShipmentParameters, getShipmentTrackingParameters, generateInvoiceQrCodeParameters, + generateInvoiceNumberParameters, createOrderParameters, getOrderParameters, updateShipmentTrackingParameters, @@ -214,6 +216,17 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'generate_invoice_number', + name: 'Generate Invoice Number', + description: generateInvoiceNumberPrompt(context), + parameters: generateInvoiceNumberParameters(context), + actions: { + invoices: { + generateInvoiceNumber: true, + }, + }, + }, { method: 'create_product', name: 'Create Product', From 02be4b0ef4cccad850e610d4701e9d486850cd6b Mon Sep 17 00:00:00 2001 From: shrir-j Date: Sat, 8 Aug 2026 15:58:59 +0530 Subject: [PATCH 2/9] Add invoice and recurring search tool --- .../shared/invoices/parameters.py | 206 +++++++++++++++++- .../shared/invoices/prompts.py | 6 + .../shared/invoices/tool_handlers.py | 28 +++ python/paypal_agent_toolkit/shared/regex.py | 1 + python/paypal_agent_toolkit/shared/tools.py | 11 + typescript/src/shared/api.ts | 3 + typescript/src/shared/functions.ts | 94 +++++++- typescript/src/shared/parameters.ts | 130 ++++++++++- typescript/src/shared/prompts.ts | 6 + typescript/src/shared/regex.ts | 1 + typescript/src/shared/tools.ts | 13 ++ 11 files changed, 496 insertions(+), 3 deletions(-) diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 35230c88..b9ac4d18 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -1,9 +1,10 @@ -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, model_validator, field_validator from typing import List, Optional, Literal from ..regex import ( INVOICE_ID_REGEX, HEX_COLOR_REGEX, DATE_NO_TIME_REGEX, + DATE_TIME_REGEX, RECURRING_SERIES_ID_REGEX, COUNTRY_CODE_REGEX, LANGUAGE_REGEX, @@ -18,6 +19,28 @@ class Money(BaseModel): value: str = Field(..., pattern=DECIMAL_STRING_REGEX.pattern, description="The amount, as a signed decimal string with up to 2 decimal places (e.g. '50.00').") +class AmountRange(BaseModel): + lower_amount: Money = Field(..., description="The lower bound of the amount range.") + upper_amount: Money = Field(..., description="The upper bound of the amount range.") + + +class DateRange(BaseModel): + start: str = Field(..., pattern=DATE_NO_TIME_REGEX.pattern, description="The start date, in yyyy-MM-DD format.") + end: str = Field(..., pattern=DATE_NO_TIME_REGEX.pattern, description="The end date, in yyyy-MM-DD format.") + + +class DateTimeRange(BaseModel): + start: str = Field(..., pattern=DATE_TIME_REGEX.pattern, min_length=20, max_length=64, description="The start date and time, in ISO8601 format (for example, 2018-06-01T00:00:00Z). Seconds are required; fractional seconds are optional. A plain date (yyyy-MM-DD) is also accepted and is expanded to the start of that day (00:00:00Z).") + end: str = Field(..., pattern=DATE_TIME_REGEX.pattern, min_length=20, max_length=64, description="The end date and time, in ISO8601 format (for example, 2018-06-21T23:59:59Z). Seconds are required; fractional seconds are optional. A plain date (yyyy-MM-DD) is also accepted and is expanded to the end of that day (23:59:59Z).") + + @field_validator("start", "end", mode="before") + @classmethod + def _expand_date_only(cls, v, info): + if isinstance(v, str) and DATE_NO_TIME_REGEX.match(v): + return f"{v}T00:00:00Z" if info.field_name == "start" else f"{v}T23:59:59Z" + return v + + class PersonName(BaseModel): given_name: Optional[str] = Field(None, description="The first name of the person.") surname: Optional[str] = Field(None, description="The last name of the person.") @@ -239,3 +262,184 @@ class UpdateInvoiceAutoReminderParameters(BaseModel): notification: Optional[ReminderNotification] = Field(None, description="Notification settings for the reminder.") +# ---- search_invoicing: one external tool that internally branches to invoice search or ---- +# ---- recurring-series search based on resource_type. invoice_filters/recurring_series_filters ---- +# ---- map 1:1 onto PayPal's real request bodies for /v2/invoicing/search-invoices and ---- +# ---- /v2/invoicing/search-recurring-invoices respectively -- no payload reshaping needed. ---- + +# Some MCP clients "clear" a filter by blanking its leaf values (e.g. a range's start/end, +# or an array/string field) instead of omitting the field entirely -- these helpers treat +# such blanked-out values as not provided, used from field_validator(mode="before") below. + +def _blank_range_to_none(v): + if isinstance(v, dict): + if not v.get("start") or not v.get("end"): + return None + return v + + +def _blank_list_to_none(v): + if isinstance(v, list) and len(v) == 0: + return None + return v + + +def _blank_str_to_none(v): + if isinstance(v, str) and not v.strip(): + return None + return v + + +def _is_empty_filters(value): + if value is None: + return True + if isinstance(value, BaseModel): + return _is_empty_filters(value.model_dump()) + if isinstance(value, str): + return not value.strip() + if isinstance(value, dict): + return all(_is_empty_filters(v) for v in value.values()) + if isinstance(value, list): + return len(value) == 0 + return False + + +class SearchInvoicesFilters(BaseModel): + recipient_email: Optional[str] = Field(None, max_length=254, description="Filters the search by the recipient's email address.") + recipient_first_name: Optional[str] = Field(None, max_length=140, description="Filters the search by the recipient's first name.") + recipient_last_name: Optional[str] = Field(None, max_length=140, description="Filters the search by the recipient's last name.") + recipient_business_name: Optional[str] = Field(None, max_length=300, description="Filters the search by the recipient's business name.") + invoice_number: Optional[str] = Field(None, max_length=25, description="Filters the search by the invoice number.") + status: Optional[List[Literal[ + "DRAFT", "SENT", "SCHEDULED", "PAID", "MARKED_AS_PAID", "CANCELLED", "REFUNDED", + "PARTIALLY_PAID", "PARTIALLY_REFUNDED", "MARKED_AS_REFUNDED", "UNPAID", "PAYMENT_PENDING", + "AUTO_CANCELLED", "PAID_EXTERNAL", "REFUNDED_EXTERNAL", "SHARED", + ]]] = Field(None, max_length=5, description="Filters the search by up to 5 invoice status values.") + reference: Optional[str] = Field(None, max_length=120, description="Filters the search by reference data, such as a purchase order (PO) number.") + currency_code: Optional[str] = Field(None, description="The three-character ISO-4217 currency code that identifies the currency.") + total_amount_range: Optional[AmountRange] = Field(None, description="Filters the search by a range of invoice total amounts.") + invoice_date_range: Optional[DateRange] = Field(None, description="Filters the search by the invoice's own date (the date shown on the invoice itself, also called the billing date). Use this for most 'invoices dated/created/issued between X and Y' requests -- creation_date_range is for PayPal's internal record-creation timestamp, not the invoice's date.") + due_date_range: Optional[DateRange] = Field(None, description="Filters the search by the invoice's due date.") + payment_date_range: Optional[DateTimeRange] = Field(None, description="Filters the search by the date and time PayPal recorded the invoice as paid (a system timestamp).") + creation_date_range: Optional[DateTimeRange] = Field(None, description="Filters the search by the date and time PayPal's system recorded the invoice record as created (an internal system timestamp, NOT the invoice's own date). For 'invoices dated/created between X and Y' requests, prefer invoice_date_range unless the user specifically means when the record was created in PayPal.") + + @field_validator("total_amount_range", mode="before") + @classmethod + def _drop_blank_amount_range(cls, v): + if isinstance(v, dict): + lower_value = (v.get("lower_amount") or {}).get("value") + upper_value = (v.get("upper_amount") or {}).get("value") + if not lower_value or not upper_value: + return None + return v + + @field_validator("invoice_date_range", "due_date_range", "payment_date_range", "creation_date_range", mode="before") + @classmethod + def _drop_blank_date_ranges(cls, v): + return _blank_range_to_none(v) + + @field_validator( + "recipient_email", "recipient_first_name", "recipient_last_name", + "recipient_business_name", "invoice_number", "reference", "currency_code", + mode="before", + ) + @classmethod + def _drop_blank_strings(cls, v): + return _blank_str_to_none(v) + + @field_validator("status", mode="before") + @classmethod + def _drop_blank_status(cls, v): + return _blank_list_to_none(v) + + +class RecurringSeriesSearchFilters(BaseModel): + currency_code: Optional[str] = Field(None, description="The three-character ISO-4217 currency code that identifies the currency.") + status: Optional[List[Literal["DRAFT", "ACTIVE", "CANCELLED", "EXPIRED"]]] = Field(None, min_length=1, max_length=5, description="An array of up to 5 unique recurring invoice series status values.") + creation_date_range: Optional[DateTimeRange] = Field(None, description="Filters the search by the date and time PayPal recorded the recurring series as created (a system timestamp). This endpoint has no separate 'series date' field -- for 'series created/started between X and Y' requests, this is the field to use. Mutually exclusive with next_occurrence_date_range -- PayPal supports only one range criterion per search.") + next_occurrence_date_range: Optional[DateRange] = Field(None, description="Filters the search by the date range of the series' next occurrence. Mutually exclusive with creation_date_range -- PayPal supports only one range criterion per search.") + total_amount_range: Optional[AmountRange] = Field(None, description="Filters the search by a range of total amounts.") + + @field_validator("status", mode="before") + @classmethod + def _drop_blank_status(cls, v): + return _blank_list_to_none(v) + + @field_validator("currency_code", mode="before") + @classmethod + def _drop_blank_currency_code(cls, v): + return _blank_str_to_none(v) + + @field_validator("status") + @classmethod + def _check_status_unique(cls, v): + if v is not None and len(set(v)) != len(v): + raise ValueError("status values must be unique.") + return v + + @field_validator("creation_date_range", "next_occurrence_date_range", mode="before") + @classmethod + def _drop_blank_date_ranges(cls, v): + return _blank_range_to_none(v) + + @field_validator("total_amount_range", mode="before") + @classmethod + def _drop_blank_amount_range(cls, v): + if isinstance(v, dict): + lower_value = (v.get("lower_amount") or {}).get("value") + upper_value = (v.get("upper_amount") or {}).get("value") + if not lower_value or not upper_value: + return None + return v + + @model_validator(mode="after") + def _check_date_range_mutually_exclusive(self): + if self.creation_date_range is not None and self.next_occurrence_date_range is not None: + raise ValueError("creation_date_range and next_occurrence_date_range cannot both be set -- PayPal supports only one range criterion per search.") + return self + + +class SearchRecurringSeriesFilters(BaseModel): + search_text: Optional[str] = Field(None, min_length=3, max_length=800, description="Free-text search, checked against the fields listed in search_fields.") + search_fields: Optional[List[Literal[ + "PAYER_REFERENCE_INFO", "BILLING_EMAIL", "BILLING_NAME", "BILLING_BUSINESS_NAME", + "BILLING_PHONE_NUMBER", "SHIPPING_NAME", "SHIPPING_BUSINESS_NAME", "SHIPPING_PHONE_NUMBER", + "ITEM_NAME", "ITEM_TAX_NAME", "ITEM_DISCOUNT_NAME", "INVOICE_DISCOUNT_NAME", "ALL", + ]]] = Field(None, min_length=1, max_length=5, description="The fields search_text is checked against. Use ['ALL'] to search every available field.") + search_filters: Optional[RecurringSeriesSearchFilters] = Field(None, description="Structured filters for the recurring series search.") + + @field_validator("search_text", mode="before") + @classmethod + def _drop_blank_search_text(cls, v): + return _blank_str_to_none(v) + + @field_validator("search_fields", mode="before") + @classmethod + def _drop_blank_search_fields(cls, v): + return _blank_list_to_none(v) + + @field_validator("search_fields") + @classmethod + def _check_search_fields_unique(cls, v): + if v is not None and len(set(v)) != len(v): + raise ValueError("search_fields values must be unique.") + return v + + +class SearchInvoicingParameters(BaseModel): + resource_type: Literal["invoice", "recurring_series"] = Field(..., description="Which kind of resource to search. 'invoice' searches individual invoices; 'recurring_series' searches recurring invoice series.") + page: Optional[int] = Field(1, ge=1, le=1000, description="The page number of the result set to fetch.") + page_size: Optional[int] = Field(20, ge=1, le=100, description="The number of records to return per page (maximum 100).") + total_required: Optional[bool] = Field(False, description="Indicates whether the response should include total_pages and total_items. Only applies when resource_type is 'invoice'.") + invoice_filters: Optional[SearchInvoicesFilters] = Field(None, description="Search filters for invoices. Set only when resource_type is 'invoice'.") + recurring_series_filters: Optional[SearchRecurringSeriesFilters] = Field(None, description="Search filters for recurring invoice series. Set only when resource_type is 'recurring_series'.") + + @model_validator(mode="after") + def _check_filters_match_resource_type(self): + if self.resource_type == "invoice" and not _is_empty_filters(self.recurring_series_filters): + raise ValueError("recurring_series_filters cannot be set when resource_type is 'invoice' -- use invoice_filters instead.") + if self.resource_type == "recurring_series" and not _is_empty_filters(self.invoice_filters): + raise ValueError("invoice_filters cannot be set when resource_type is 'recurring_series' -- use recurring_series_filters instead.") + return self + + diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index 57419388..5ff1147f 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -74,4 +74,10 @@ Update an existing invoice auto reminder configuration by its configuration ID. This function performs a full update of the reminder configuration's timing interval, repetition count and notification preferences. +""" + +SEARCH_INVOICING_PROMPT = """ +Search for invoices or recurring invoice series on PayPal. + +Use resource_type "invoice" with invoice_filters, or "recurring_series" with recurring_series_filters -- only set the matching filters object. Recurring series search covers only the past 3 years. """ \ No newline at end of file diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index ba7ea4c9..a3e743c0 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -181,3 +181,31 @@ def update_invoice_auto_reminder(client, params: dict): response = client.put(uri=url, payload=payload, headers={"Prefer": "return=representation"}) return json.dumps(response) + + +def search_invoicing(client, params: dict): + + validated = SearchInvoicingParameters(**params) + + if validated.resource_type == "invoice": + return _search_invoices(client, validated) + return _search_recurring_series(client, validated) + + +def _search_invoices(client, validated: SearchInvoicingParameters): + + body = (validated.invoice_filters or SearchInvoicesFilters()).model_dump(exclude_none=True) + total_required = "true" if validated.total_required else "false" + uri = f"/v2/invoicing/search-invoices?page={validated.page}&page_size={validated.page_size}&total_required={total_required}" + response = client.post(uri=uri, payload=body) + + return json.dumps(response) + + +def _search_recurring_series(client, validated: SearchInvoicingParameters): + + body = (validated.recurring_series_filters or SearchRecurringSeriesFilters()).model_dump(exclude_none=True) + uri = f"/v2/invoicing/search-recurring-invoices?page={validated.page}&page_size={validated.page_size}" + response = client.post(uri=uri, payload=body) + + return json.dumps(response) diff --git a/python/paypal_agent_toolkit/shared/regex.py b/python/paypal_agent_toolkit/shared/regex.py index 9f166b16..cddf5016 100644 --- a/python/paypal_agent_toolkit/shared/regex.py +++ b/python/paypal_agent_toolkit/shared/regex.py @@ -17,6 +17,7 @@ TRANSACTION_ID_REGEX = re.compile(r"^[A-Za-z0-9_-]{12,255}$") HEX_COLOR_REGEX = re.compile(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") DATE_NO_TIME_REGEX = re.compile(r"^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$") +DATE_TIME_REGEX = re.compile(r"^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$") RECURRING_SERIES_ID_REGEX = re.compile(r"^RI-[A-Z0-9]{17}$") COUNTRY_CODE_REGEX = re.compile(r"^([A-Z]{2}|C2)$") LANGUAGE_REGEX = re.compile(r"^[a-z]{2}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$") diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index 424229a3..c102d99b 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -28,6 +28,7 @@ GENERATE_INVOICE_NUMBER_PROMPT, SETUP_INVOICE_AUTO_REMINDER_PROMPT, UPDATE_INVOICE_AUTO_REMINDER_PROMPT, + SEARCH_INVOICING_PROMPT, ) from ..shared.disputes.prompts import ( @@ -82,6 +83,7 @@ GenerateInvoiceNumberParameters, SetupInvoiceAutoReminderParameters, UpdateInvoiceAutoReminderParameters, + SearchInvoicingParameters, ) from ..shared.disputes.parameters import ( @@ -135,6 +137,7 @@ generate_invoice_number, setup_invoice_auto_reminders, update_invoice_auto_reminder, + search_invoicing, ) @@ -353,6 +356,14 @@ "actions": {"invoices": {"updateReminder": True}}, "execute": update_invoice_auto_reminder, }, + { + "method": "search_invoicing", + "name": "Search Invoices or Recurring Invoice Series", + "description": SEARCH_INVOICING_PROMPT.strip(), + "args_schema": SearchInvoicingParameters, + "actions": {"invoices": {"search": True}}, + "execute": search_invoicing, + }, { "method": "list_disputes", "name": "List Disputes", diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index 0560b9db..0cb008cf 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -9,6 +9,7 @@ import { cancelSentInvoice, setupInvoiceAutoReminder, updateInvoiceAutoReminder, + searchInvoicing, createProduct, listProducts, createSubscriptionPlan, @@ -110,6 +111,8 @@ class PayPalAPI { return setupInvoiceAutoReminder(this.paypalClient, this.context, arg); case 'update_invoice_auto_reminder': return updateInvoiceAutoReminder(this.paypalClient, this.context, arg); + case 'search_invoicing': + return searchInvoicing(this.paypalClient, this.context, arg); case 'generate_invoice_qr_code': return generateInvoiceQrCode(this.paypalClient, this.context, arg); case 'generate_invoice_number': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index ba8b11cd..d9d4bbdf 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -36,7 +36,8 @@ import { updatePlanParameters, getMerchantInsightsParameters, setupInvoiceAutoReminderParameters, - updateInvoiceAutoReminderParameters + updateInvoiceAutoReminderParameters, + searchInvoicingParameters } from "./parameters"; import {parseOrderDetails, parseUpdateSubscriptionPayload, buildCreateInvoicePayload, buildCreateRecurringSeriesPayload, toQueryString} from "./payloadUtils"; import { TypeOf } from "zod"; @@ -323,6 +324,97 @@ export async function updateInvoiceAutoReminder( } } +// Some MCP clients keep a previous call's filters object attached (now fully blanked out) +// after switching resource_type, instead of removing the key -- treat a filters object with +// no actual content as equivalent to not having been provided at all. +function isEmptyFilters(value: unknown): boolean { + if (value === undefined || value === null) return true; + if (typeof value === 'string') return value.trim().length === 0; + if (Array.isArray(value)) return value.length === 0; + if (typeof value === 'object') return Object.values(value as Record).every(isEmptyFilters); + return false; +} + +// search_invoicing: one external tool that internally branches to invoice search or +// recurring-series search based on resource_type. invoice_filters/recurring_series_filters +// map 1:1 onto PayPal's real request bodies for /v2/invoicing/search-invoices and +// /v2/invoicing/search-recurring-invoices respectively -- no payload reshaping needed. +export async function searchInvoicing( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + const { resource_type, recurring_series_filters, invoice_filters } = params; + + if (resource_type === 'invoice' && !isEmptyFilters(recurring_series_filters)) { + throw new Error("recurring_series_filters cannot be set when resource_type is 'invoice' -- use invoice_filters instead."); + } + if (resource_type === 'recurring_series' && !isEmptyFilters(invoice_filters)) { + throw new Error("invoice_filters cannot be set when resource_type is 'recurring_series' -- use recurring_series_filters instead."); + } + + if (resource_type === 'invoice') { + return searchInvoices(client, params); + } + return searchRecurringSeries(client, params); +} + +async function searchInvoices( + client: PayPalClient, + params: TypeOf> +) { + logger('[searchInvoices] Starting to search invoices'); + + const { page, page_size, total_required, invoice_filters } = params; + + const headers = await client.getHeaders(); + logger('[searchInvoices] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/search-invoices`; + logger(`[searchInvoices] API URL: ${url}`); + + try { + logger('[searchInvoices] Sending request to PayPal API'); + const response = await axios.post(url, invoice_filters ?? {}, { + headers, + params: { page, page_size, total_required }, + }); + logger(`[searchInvoices] Invoices retrieved successfully. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[searchInvoices] Error searching invoices:', error.message); + handleAxiosError(error); + } +} + +async function searchRecurringSeries( + client: PayPalClient, + params: TypeOf> +) { + logger('[searchRecurringSeries] Starting to search recurring invoice series'); + + const { page, page_size, recurring_series_filters } = params; + + const headers = await client.getHeaders(); + logger('[searchRecurringSeries] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/search-recurring-invoices`; + logger(`[searchRecurringSeries] API URL: ${url}`); + + try { + logger('[searchRecurringSeries] Sending request to PayPal API'); + const response = await axios.post(url, recurring_series_filters ?? {}, { + headers, + params: { page, page_size }, + }); + logger(`[searchRecurringSeries] Recurring invoice series retrieved successfully. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[searchRecurringSeries] Error searching recurring invoice series:', error.message); + handleAxiosError(error); + } +} + export async function cancelSentInvoice( client: PayPalClient, context: Context, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index e726f22e..17b915c9 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import type { Context } from './configuration'; import {subscriptionKeys} from "./constants"; -import {INVOICE_ID_REGEX, ORDER_ID_REGEX, SUBSCRIPTION_ID_REGEX, PRODUCT_ID_REGEX, PLAN_ID_REGEX, DISPUTE_ID_REGEX, REFUND_ID_REGEX, CAPTURE_ID_REGEX, TRANSACTION_ID_REGEX, HEX_COLOR_REGEX, DATE_NO_TIME_REGEX, RECURRING_SERIES_ID_REGEX, COUNTRY_CODE_REGEX, LANGUAGE_REGEX, DECIMAL_STRING_REGEX} from "./regex" +import {INVOICE_ID_REGEX, ORDER_ID_REGEX, SUBSCRIPTION_ID_REGEX, PRODUCT_ID_REGEX, PLAN_ID_REGEX, DISPUTE_ID_REGEX, REFUND_ID_REGEX, CAPTURE_ID_REGEX, TRANSACTION_ID_REGEX, HEX_COLOR_REGEX, DATE_NO_TIME_REGEX, DATE_TIME_REGEX, RECURRING_SERIES_ID_REGEX, COUNTRY_CODE_REGEX, LANGUAGE_REGEX, DECIMAL_STRING_REGEX} from "./regex" // === INVOICE PARAMETERS === // Shared building blocks, reused across create_invoice and create_recurring_series (and @@ -20,6 +20,58 @@ const money = () => z.object({ value: z.string().regex(DECIMAL_STRING_REGEX, "value must be a numeric value, e.g. \"50\" or \"50.00\"").describe("The amount, as a signed decimal string with up to 2 decimal places (e.g. '50.00')."), }); +const amountRange = () => z.object({ + lower_amount: money().describe("The lower bound of the amount range."), + upper_amount: money().describe("The upper bound of the amount range."), +}).describe("amount range object"); + +// Some MCP clients "clear" a filter by blanking out its leaf values (e.g. lower_amount.value: "") +// instead of omitting the field entirely -- treat a range with a blank bound as not provided. +const amountRangeOrUndefinedIfBlank = (description: string) => z.preprocess( + (val) => { + if (val && typeof val === "object") { + const lowerValue = (val as any).lower_amount?.value; + const upperValue = (val as any).upper_amount?.value; + if (!lowerValue || !upperValue) return undefined; + } + return val; + }, + amountRange().optional().describe(description) +); + +const dateRange = () => z.object({ + start: z.string().regex(DATE_NO_TIME_REGEX, "start must be in yyyy-MM-DD format").describe("The start date, in yyyy-MM-DD format."), + end: z.string().regex(DATE_NO_TIME_REGEX, "end must be in yyyy-MM-DD format").describe("The end date, in yyyy-MM-DD format."), +}).describe("date range object"); + +// Some MCP clients "clear" a filter by blanking out a range's leaf values (e.g. start: "") +// instead of omitting the field entirely -- treat a range with a blank bound as not provided. +const dateRangeOrUndefinedIfBlank = (description: string) => z.preprocess( + (val) => { + if (val && typeof val === "object" && (!(val as any).start || !(val as any).end)) return undefined; + return val; + }, + dateRange().optional().describe(description) +); + +const dateTimeBoundary = (defaultTime: string, label: string, description: string) => z.preprocess( + (val) => (typeof val === "string" && DATE_NO_TIME_REGEX.test(val) ? `${val}T${defaultTime}` : val), + z.string().regex(DATE_TIME_REGEX, `${label} must be in ISO8601 format, e.g. 2018-06-01T00:00:00Z`).min(20).max(64).describe(description) +); + +const dateTimeRange = () => z.object({ + start: dateTimeBoundary("00:00:00Z", "start", "The start date and time, in ISO8601 format (for example, 2018-06-01T00:00:00Z). Seconds are required; fractional seconds are optional. A plain date (yyyy-MM-DD) is also accepted and is expanded to the start of that day (00:00:00Z)."), + end: dateTimeBoundary("23:59:59Z", "end", "The end date and time, in ISO8601 format (for example, 2018-06-21T23:59:59Z). Seconds are required; fractional seconds are optional. A plain date (yyyy-MM-DD) is also accepted and is expanded to the end of that day (23:59:59Z)."), +}).describe("date and time range object"); + +const dateTimeRangeOrUndefinedIfBlank = (description: string) => z.preprocess( + (val) => { + if (val && typeof val === "object" && (!(val as any).start || !(val as any).end)) return undefined; + return val; + }, + dateTimeRange().optional().describe(description) +); + const personName = () => z.object({ given_name: z.string().optional().describe("The first name of the person."), surname: z.string().optional().describe("The last name of the person."), @@ -245,6 +297,82 @@ export const updateInvoiceAutoReminderParameters = (context: Context) => }).describe('Full replacement configuration for an existing invoice auto reminder. All required fields must be included since this performs a full update.'); +// search_invoicing: one external tool that internally branches to invoice search or +// recurring-series search based on resource_type. invoice_filters/recurring_series_filters +// map 1:1 onto PayPal's real request bodies for /v2/invoicing/search-invoices and +// /v2/invoicing/search-recurring-invoices respectively -- no payload reshaping needed. + +// Some MCP clients "clear" an array/string filter by blanking it to [] or "" instead of +// omitting the field entirely -- these treat such blanked-out values as not provided. +const arrayOrUndefinedIfEmpty = (schema: z.ZodTypeAny) => z.preprocess( + (val) => (Array.isArray(val) && val.length === 0 ? undefined : val), + schema +); + +const stringOrUndefinedIfBlank = (schema: z.ZodTypeAny) => z.preprocess( + (val) => (typeof val === "string" && val.trim().length === 0 ? undefined : val), + schema +); + +const searchInvoicesFilters = () => z.object({ + recipient_email: stringOrUndefinedIfBlank(z.string().max(254).optional().describe("Filters invoices by the recipient's email address.")), + recipient_first_name: stringOrUndefinedIfBlank(z.string().max(140).optional().describe("Filters invoices by the recipient's first name.")), + recipient_last_name: stringOrUndefinedIfBlank(z.string().max(140).optional().describe("Filters invoices by the recipient's last name.")), + recipient_business_name: stringOrUndefinedIfBlank(z.string().max(300).optional().describe("Filters invoices by the recipient's business name.")), + invoice_number: stringOrUndefinedIfBlank(z.string().max(25).optional().describe("Filters invoices by invoice number.")), + status: arrayOrUndefinedIfEmpty(z.array(z.enum([ + "DRAFT", "SENT", "SCHEDULED", "PAID", "MARKED_AS_PAID", "CANCELLED", "REFUNDED", + "PARTIALLY_PAID", "PARTIALLY_REFUNDED", "MARKED_AS_REFUNDED", "UNPAID", "PAYMENT_PENDING", + "AUTO_CANCELLED", "PAID_EXTERNAL", "REFUNDED_EXTERNAL", "SHARED", + ])).max(5).optional().describe("Filters invoices by one or more statuses (up to 5).")), + reference: stringOrUndefinedIfBlank(z.string().max(120).optional().describe("Filters invoices by reference value, such as a purchase order number.")), + currency_code: stringOrUndefinedIfBlank(z.string().optional().describe("Filters invoices by the three-character ISO-4217 currency code.")), + total_amount_range: amountRangeOrUndefinedIfBlank("Filters invoices whose total amount falls within this range."), + invoice_date_range: dateRangeOrUndefinedIfBlank("Filters invoices by the invoice's own date (the date shown on the invoice itself, also called the billing date). Use this for most 'invoices dated/created/issued between X and Y' requests -- creation_date_range is for PayPal's internal record-creation timestamp, not the invoice's date."), + due_date_range: dateRangeOrUndefinedIfBlank("Filters invoices whose due date falls within this range."), + payment_date_range: dateTimeRangeOrUndefinedIfBlank("Filters invoices by the date and time PayPal recorded the invoice as paid (a system timestamp)."), + creation_date_range: dateTimeRangeOrUndefinedIfBlank("Filters invoices by the date and time PayPal's system recorded the invoice record as created (an internal system timestamp, NOT the invoice's own date). For 'invoices dated/created between X and Y' requests, prefer invoice_date_range unless the user specifically means when the record was created in PayPal."), +}).describe("Filters for searching individual invoices."); + +const recurringSeriesSearchFilters = () => z.object({ + currency_code: stringOrUndefinedIfBlank(z.string().optional().describe("Filters recurring series by the three-character ISO-4217 currency code.")), + status: arrayOrUndefinedIfEmpty(z.array(z.enum(["DRAFT", "ACTIVE", "CANCELLED", "EXPIRED"])).min(1).max(5).optional().describe("Filters recurring series by one or more statuses (up to 5, must be unique).")), + creation_date_range: dateTimeRangeOrUndefinedIfBlank("Filters recurring series by the date and time PayPal recorded the series as created (a system timestamp). This endpoint has no separate 'series date' field -- for 'series created/started between X and Y' requests, this is the field to use. Mutually exclusive with next_occurrence_date_range -- PayPal supports only one range criterion per search."), + next_occurrence_date_range: dateRangeOrUndefinedIfBlank("Filters recurring series whose next occurrence date falls within this range. Mutually exclusive with creation_date_range -- PayPal supports only one range criterion per search."), + total_amount_range: amountRangeOrUndefinedIfBlank("Filters recurring series whose total amount falls within this range."), +}).refine((val) => val.status === undefined || new Set(val.status).size === val.status.length, { + message: "status values must be unique.", + path: ["status"], +}).refine((val) => val.creation_date_range === undefined || val.next_occurrence_date_range === undefined, { + message: "creation_date_range and next_occurrence_date_range cannot both be set -- PayPal supports only one range criterion per search.", + path: ["next_occurrence_date_range"], +}).describe("Structured filters for searching recurring invoice series."); + +const searchRecurringSeriesFilters = () => z.object({ + search_text: stringOrUndefinedIfBlank(z.string().min(3).max(800).optional().describe("Free-text search across the fields listed in search_fields. Cannot be blank.")), + search_fields: arrayOrUndefinedIfEmpty(z.array(z.enum([ + "PAYER_REFERENCE_INFO", "BILLING_EMAIL", "BILLING_NAME", "BILLING_BUSINESS_NAME", + "BILLING_PHONE_NUMBER", "SHIPPING_NAME", "SHIPPING_BUSINESS_NAME", "SHIPPING_PHONE_NUMBER", + "ITEM_NAME", "ITEM_TAX_NAME", "ITEM_DISCOUNT_NAME", "INVOICE_DISCOUNT_NAME", "ALL", + ])).min(1).max(5).optional().describe("The fields that search_text searches against (1-5 values, must be unique).")), + search_filters: recurringSeriesSearchFilters().optional().describe("Structured filters for the recurring series search."), +}).refine((val) => val.search_fields === undefined || new Set(val.search_fields).size === val.search_fields.length, { + message: "search_fields values must be unique.", + path: ["search_fields"], +}).describe("Filters for searching recurring invoice series."); + +// Note: the resource_type <-> filters pairing is validated at runtime in searchInvoicing() +// (functions.ts), not via .refine() here -- Tool.parameters requires a plain z.ZodObject +// (see tools.ts), and .refine() would wrap this in a ZodEffects that no longer satisfies it. +export const searchInvoicingParameters = (context: Context) => z.object({ + resource_type: z.enum(["invoice", "recurring_series"]).describe("Which resource type to search. 'invoice' searches individual invoices using invoice_filters; 'recurring_series' searches recurring invoice series using recurring_series_filters."), + page: z.number().int().min(1).max(1000).default(1).optional().describe("The page number of the result set to fetch."), + page_size: z.number().int().min(1).max(100).default(20).optional().describe("The number of records to return per page (maximum 100)."), + total_required: z.boolean().default(false).optional().describe("Indicates whether the response should include the total count of matching invoices. Only applies when resource_type is 'invoice'."), + invoice_filters: searchInvoicesFilters().optional().describe("Filters to apply when resource_type is 'invoice'. Must not be set when resource_type is 'recurring_series'."), + recurring_series_filters: searchRecurringSeriesFilters().optional().describe("Filters to apply when resource_type is 'recurring_series'. Must not be set when resource_type is 'invoice'."), +}).describe("Search for invoices or recurring invoice series, depending on resource_type."); + export const updateProductParameters = (context: Context) => z.object({ product_id: z.string().describe('The ID of the product to update.'), diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index 61e89329..004cc8b2 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -68,6 +68,12 @@ Update an existing invoice auto reminder configuration by its configuration ID. This function performs a full update of the reminder configuration's timing interval, repetition count and notification preferences. `; +export const searchInvoicingPrompt = (context: Context) => ` +Search for invoices or recurring invoice series on PayPal. + +Use resource_type "invoice" with invoice_filters, or "recurring_series" with recurring_series_filters -- only set the matching filters object. Recurring series search covers only the past 3 years. +`; + export const generateInvoiceQrCodePrompt = (context: Context) => ` Generate a QR code for an invoice. diff --git a/typescript/src/shared/regex.ts b/typescript/src/shared/regex.ts index 77b2204a..9623d679 100644 --- a/typescript/src/shared/regex.ts +++ b/typescript/src/shared/regex.ts @@ -9,6 +9,7 @@ export const CAPTURE_ID_REGEX = /^[A-Za-z0-9_-]{15,32}$/; export const TRANSACTION_ID_REGEX = /^[A-Za-z0-9_-]{12,255}$/; export const HEX_COLOR_REGEX = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; export const DATE_NO_TIME_REGEX = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/; +export const DATE_TIME_REGEX = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$/; export const RECURRING_SERIES_ID_REGEX = /^RI-[A-Z0-9]{17}$/; export const COUNTRY_CODE_REGEX = /^([A-Z]{2}|C2)$/; export const LANGUAGE_REGEX = /^[a-z]{2}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$/; diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index 71dde849..00fa2a13 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -11,6 +11,7 @@ import { cancelSentInvoicePrompt, setupInvoiceAutoReminderPrompt, updateInvoiceAutoReminderPrompt, + searchInvoicingPrompt, createShipmentPrompt, getShipmentTrackingPrompt, generateInvoiceQrCodePrompt, @@ -51,6 +52,7 @@ import { cancelSentInvoiceParameters, setupInvoiceAutoReminderParameters, updateInvoiceAutoReminderParameters, + searchInvoicingParameters, createShipmentParameters, getShipmentTrackingParameters, generateInvoiceQrCodeParameters, @@ -205,6 +207,17 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'search_invoicing', + name: 'Search Invoices or Recurring Invoice Series', + description: searchInvoicingPrompt(context), + parameters: searchInvoicingParameters(context), + actions: { + invoices: { + search: true, + }, + }, + }, { method: 'generate_invoice_qr_code', name: 'Generate Invoice QR Code', From 47b64b009a288576e421c0df5b90311aeca76d8b Mon Sep 17 00:00:00 2001 From: shrir-j Date: Sat, 8 Aug 2026 17:05:44 +0530 Subject: [PATCH 3/9] Add invoice delete tool --- .../shared/invoices/parameters.py | 4 +++ .../shared/invoices/prompts.py | 6 ++++ .../shared/invoices/tool_handlers.py | 14 +++++++++ .../shared/paypal_client.py | 31 +++++++++++++++++-- python/paypal_agent_toolkit/shared/tools.py | 11 +++++++ typescript/src/shared/api.ts | 3 ++ typescript/src/shared/functions.ts | 28 +++++++++++++++++ typescript/src/shared/parameters.ts | 4 +++ typescript/src/shared/prompts.ts | 6 ++++ typescript/src/shared/tools.ts | 13 ++++++++ 10 files changed, 118 insertions(+), 2 deletions(-) diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 7c369826..0d9537cf 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -193,6 +193,10 @@ class CancelSentInvoiceParameters(BaseModel): additional_recipients: Optional[List[str]] = Field(None, description="Additional email addresses to which to send the cancellation.") +class DeleteInvoiceParameters(BaseModel): + invoice_id: str = Field(..., description="The ID of the draft or scheduled invoice to delete.", pattern=INVOICE_ID_REGEX.pattern) + + class GenerateInvoiceQrCodeParameters(BaseModel): invoice_id: str = Field(..., description="The invoice id to generate QR code for", pattern=INVOICE_ID_REGEX) width: int = Field(300, description="The QR code width") diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index f0a62b88..e3c63756 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -52,6 +52,12 @@ This function cancels an invoice that has already been sent to the recipient(s). """ +DELETE_INVOICE_PROMPT = """ +Delete a draft or scheduled invoice on PayPal. + +This function permanently deletes an invoice that is in the draft or scheduled state, by ID. It does not work on invoices that have already been sent -- use cancel_sent_invoice for those instead. After deletion, the invoice's details can no longer be retrieved, but its invoice number can be reused. +""" + GENERATE_INVOICE_QRCODE_PROMPT = """ Generate a QR code for an invoice. diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index 624def9d..801e18d4 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -126,6 +126,20 @@ def cancel_sent_invoice(client, params: dict): return json.dumps(response) +def delete_invoice(client, params: dict): + validated = DeleteInvoiceParameters(**params) + invoice_id = validated.invoice_id + url = f"/v2/invoicing/invoices/{invoice_id}" + + response = client.delete(uri=url) + + # PayPal responds with 204 No Content on successful deletion (client.delete returns {} for that) + if not response: + return json.dumps({"success": True, "invoice_id": invoice_id}) + + return json.dumps(response) + + def generate_invoice_qrcode(client, params: dict): validated = GenerateInvoiceQrCodeParameters(**params) diff --git a/python/paypal_agent_toolkit/shared/paypal_client.py b/python/paypal_agent_toolkit/shared/paypal_client.py index 93d9145d..edbbae04 100644 --- a/python/paypal_agent_toolkit/shared/paypal_client.py +++ b/python/paypal_agent_toolkit/shared/paypal_client.py @@ -144,5 +144,32 @@ def put(self, uri, payload): return json_response - - + def delete(self, uri): + + url = f"{self.base_url}{uri}" + headers = self.build_headers() + logRequestPayload(None, url, headers) + + try: + response = requests.delete(url, headers=headers) + response.raise_for_status() + except requests.exceptions.RequestException as e: + self.log_request_exception(e, url) + raise + + if response.status_code == 204: + logging.debug("Response Status: 204 No Content") + return {} + + try: + json_response = response.json() + except ValueError: + logging.warning("Response body is not valid JSON or empty") + return {} + + logResponsePayload(response, json_response) + + return json_response + + + diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index 0b5aea90..39e39329 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -24,6 +24,7 @@ SEND_INVOICE_PROMPT, SEND_INVOICE_REMINDER_PROMPT, CANCEL_SENT_INVOICE_PROMPT, + DELETE_INVOICE_PROMPT, GENERATE_INVOICE_QRCODE_PROMPT, SETUP_INVOICE_AUTO_REMINDER_PROMPT, UPDATE_INVOICE_AUTO_REMINDER_PROMPT, @@ -77,6 +78,7 @@ GetInvoiceParameters, SendInvoiceReminderParameters, CancelSentInvoiceParameters, + DeleteInvoiceParameters, GenerateInvoiceQrCodeParameters, SetupInvoiceAutoReminderParameters, UpdateInvoiceAutoReminderParameters, @@ -129,6 +131,7 @@ get_invoice, send_invoice_reminder, cancel_sent_invoice, + delete_invoice, generate_invoice_qrcode, setup_invoice_auto_reminders, update_invoice_auto_reminder, @@ -318,6 +321,14 @@ "actions": {"invoices": {"cancel": True}}, "execute": cancel_sent_invoice, }, + { + "method": "delete_invoice", + "name": "Delete Invoice", + "description": DELETE_INVOICE_PROMPT.strip(), + "args_schema": DeleteInvoiceParameters, + "actions": {"invoices": {"delete": True}}, + "execute": delete_invoice, + }, { "method": "generate_invoice_qr_code", "name": "Generate Invoice QR Code", diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index c59bb0eb..81753638 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -7,6 +7,7 @@ import { sendInvoice, sendInvoiceReminder, cancelSentInvoice, + deleteInvoice, setupInvoiceAutoReminder, updateInvoiceAutoReminder, createProduct, @@ -105,6 +106,8 @@ class PayPalAPI { return sendInvoiceReminder(this.paypalClient, this.context, arg); case 'cancel_sent_invoice': return cancelSentInvoice(this.paypalClient, this.context, arg); + case 'delete_invoice': + return deleteInvoice(this.paypalClient, this.context, arg); case 'setup_invoice_auto_reminders': return setupInvoiceAutoReminder(this.paypalClient, this.context, arg); case 'update_invoice_auto_reminder': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index e968814e..61d93292 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -3,6 +3,7 @@ import type { Context } from './configuration'; import { getInvoicParameters, cancelSentInvoiceParameters, + deleteInvoiceParameters, createInvoiceParameters, createRecurringSeriesParameters, activateRecurringSeriesParameters, @@ -357,6 +358,33 @@ export async function cancelSentInvoice( } } +export async function deleteInvoice( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[deleteInvoice] Starting to delete invoice'); + const { invoice_id } = params; + + const headers = await client.getHeaders(); + logger('[deleteInvoice] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/invoices/${invoice_id}`; + + try { + logger('[deleteInvoice] Sending request to PayPal API'); + const response = await axios.delete(url, { headers }); + if (response.status === 204) { + logger(`[deleteInvoice] Invoice deleted successfully. Status: ${response.status}`); + return { success: true, invoice_id }; + } + return response.data; + } catch (error: any) { + logger('[deleteInvoice] Error deleting invoice:', error.message); + handleAxiosError(error); + } +} + export async function generateInvoiceQrCode( client: PayPalClient, context: Context, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index cffef739..6d5c940e 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -200,6 +200,10 @@ export const cancelSentInvoiceParameters = (context: Context) => additional_recipients: z.array(z.string()).optional().describe('Additional email addresses to which to send the cancellation.'), }); +export const deleteInvoiceParameters = (context: Context) => z.object({ + invoice_id: z.string().regex(INVOICE_ID_REGEX, "Invalid PayPal Invoice ID").describe('The ID of the draft or scheduled invoice to delete.'), +}); + export const generateInvoiceQrCodeParameters = (context: Context) => z.object({ invoice_id: z.string().regex(INVOICE_ID_REGEX, "Invalid PayPal Invoice ID").describe('The invoice id to generate QR code for'), width: z.number().default(300).describe("The QR code width"), diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index 23fdb2a4..2bc7593a 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -56,6 +56,12 @@ Cancel a sent invoice. This function cancels an invoice that has already been sent to the recipient(s). `; +export const deleteInvoicePrompt = (context: Context) => ` +Delete a draft or scheduled invoice on PayPal. + +This function permanently deletes an invoice that is in the draft or scheduled state, by ID. It does not work on invoices that have already been sent -- use cancel_sent_invoice for those instead. After deletion, the invoice's details can no longer be retrieved, but its invoice number can be reused. +`; + export const setupInvoiceAutoReminderPrompt = (context: Context) => ` Initialize the invoice auto reminder configuration for the merchant's PayPal account. diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index c7b1fee7..709f0250 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -9,6 +9,7 @@ import { sendInvoicePrompt, sendInvoiceReminderPrompt, cancelSentInvoicePrompt, + deleteInvoicePrompt, setupInvoiceAutoReminderPrompt, updateInvoiceAutoReminderPrompt, createShipmentPrompt, @@ -48,6 +49,7 @@ import { sendInvoiceParameters, sendInvoiceReminderParameters, cancelSentInvoiceParameters, + deleteInvoiceParameters, setupInvoiceAutoReminderParameters, updateInvoiceAutoReminderParameters, createShipmentParameters, @@ -181,6 +183,17 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'delete_invoice', + name: 'Delete Invoice', + description: deleteInvoicePrompt(context), + parameters: deleteInvoiceParameters(context), + actions: { + invoices: { + delete: true, + }, + }, + }, { method: 'setup_invoice_auto_reminders', name: 'Setup Invoice Auto Reminders', From fdee9ea349adb6c21d2ce7085d8a3edc815918aa Mon Sep 17 00:00:00 2001 From: shrir-j Date: Mon, 10 Aug 2026 10:41:21 +0530 Subject: [PATCH 4/9] Add cancel invoice auto reminder tool --- .../shared/invoices/parameters.py | 4 +++ .../shared/invoices/prompts.py | 6 ++++ .../shared/invoices/tool_handlers.py | 14 +++++++++ python/paypal_agent_toolkit/shared/tools.py | 11 +++++++ typescript/src/shared/api.ts | 3 ++ typescript/src/shared/functions.ts | 31 ++++++++++++++++++- typescript/src/shared/parameters.ts | 5 +++ typescript/src/shared/prompts.ts | 6 ++++ typescript/src/shared/tools.ts | 13 ++++++++ 9 files changed, 92 insertions(+), 1 deletion(-) diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 0d9537cf..2f1cb24a 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -230,6 +230,10 @@ class SetupInvoiceAutoReminderParameters(BaseModel): ) +class CancelInvoiceAutoReminderParameters(BaseModel): + invoice_id: str = Field(..., description="The ID of the invoice for which to cancel all scheduled automatic reminders.", pattern=INVOICE_ID_REGEX) + + class UpdateInvoiceAutoReminderParameters(BaseModel): reminder_configuration_id: str = Field(..., description="The ID of the auto reminder configuration to update.") type: Literal["BEFORE_DUE", "AFTER_DUE"] diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index e3c63756..40e9f65a 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -74,4 +74,10 @@ Update an existing invoice auto reminder configuration by its configuration ID. This function performs a full update of the reminder configuration's timing interval, repetition count and notification preferences. +""" + +CANCEL_INVOICE_AUTO_REMINDER_PROMPT = """ +Cancel all scheduled automatic reminders for an invoice. + +This function permanently cancels every automatic reminder scheduled for a specific invoice, by invoice ID. This action is irreversible -- once cancelled, automatic reminders cannot be re-enabled for that invoice. """ \ No newline at end of file diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index 801e18d4..35d52c09 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -184,3 +184,17 @@ def update_invoice_auto_reminder(client, params: dict): response = client.put(uri=url, payload=payload, headers={"Prefer": "return=representation"}) return json.dumps(response) + + +def cancel_invoice_auto_reminder(client, params: dict): + validated = CancelInvoiceAutoReminderParameters(**params) + invoice_id = validated.invoice_id + url = f"/v2/invoicing/invoices/{invoice_id}/cancel-reminders" + + response = client.post(uri=url, payload={}) + + # PayPal responds with 204 No Content on success (client.post returns {} for that) + if not response: + return json.dumps({"success": True, "invoice_id": invoice_id}) + + return json.dumps(response) diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index 39e39329..5e250a4a 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -28,6 +28,7 @@ GENERATE_INVOICE_QRCODE_PROMPT, SETUP_INVOICE_AUTO_REMINDER_PROMPT, UPDATE_INVOICE_AUTO_REMINDER_PROMPT, + CANCEL_INVOICE_AUTO_REMINDER_PROMPT, ) from ..shared.disputes.prompts import ( @@ -82,6 +83,7 @@ GenerateInvoiceQrCodeParameters, SetupInvoiceAutoReminderParameters, UpdateInvoiceAutoReminderParameters, + CancelInvoiceAutoReminderParameters, ) from ..shared.disputes.parameters import ( @@ -135,6 +137,7 @@ generate_invoice_qrcode, setup_invoice_auto_reminders, update_invoice_auto_reminder, + cancel_invoice_auto_reminder, ) @@ -353,6 +356,14 @@ "actions": {"invoices": {"updateReminder": True}}, "execute": update_invoice_auto_reminder, }, + { + "method": "cancel_invoice_auto_reminder", + "name": "Cancel Invoice Auto Reminder", + "description": CANCEL_INVOICE_AUTO_REMINDER_PROMPT.strip(), + "args_schema": CancelInvoiceAutoReminderParameters, + "actions": {"invoices": {"cancelReminders": True}}, + "execute": cancel_invoice_auto_reminder, + }, { "method": "list_disputes", "name": "List Disputes", diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index 81753638..f3acbb22 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -10,6 +10,7 @@ import { deleteInvoice, setupInvoiceAutoReminder, updateInvoiceAutoReminder, + cancelInvoiceAutoReminder, createProduct, listProducts, createSubscriptionPlan, @@ -112,6 +113,8 @@ class PayPalAPI { return setupInvoiceAutoReminder(this.paypalClient, this.context, arg); case 'update_invoice_auto_reminder': return updateInvoiceAutoReminder(this.paypalClient, this.context, arg); + case 'cancel_invoice_auto_reminder': + return cancelInvoiceAutoReminder(this.paypalClient, this.context, arg); case 'generate_invoice_qr_code': return generateInvoiceQrCode(this.paypalClient, this.context, arg); case 'create_product': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index 61d93292..4fc32047 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -36,7 +36,8 @@ import { updatePlanParameters, getMerchantInsightsParameters, setupInvoiceAutoReminderParameters, - updateInvoiceAutoReminderParameters + updateInvoiceAutoReminderParameters, + cancelInvoiceAutoReminderParameters } from "./parameters"; import {parseOrderDetails, parseUpdateSubscriptionPayload, buildCreateInvoicePayload, buildCreateRecurringSeriesPayload, toQueryString} from "./payloadUtils"; import { TypeOf } from "zod"; @@ -323,6 +324,34 @@ export async function updateInvoiceAutoReminder( } } +export async function cancelInvoiceAutoReminder( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[cancelInvoiceAutoReminder] Starting to cancel invoice auto reminders'); + const { invoice_id } = params; + + const headers = await client.getHeaders(); + logger('[cancelInvoiceAutoReminder] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/invoices/${invoice_id}/cancel-reminders`; + + try { + logger('[cancelInvoiceAutoReminder] Sending request to PayPal API'); + const response = await axios.post(url, {}, { headers }); + if (response.status === 204) { + logger(`[cancelInvoiceAutoReminder] Invoice auto reminders cancelled successfully. Status: ${response.status}`); + return { success: true, invoice_id }; + } + logger(`[cancelInvoiceAutoReminder] Invoice auto reminders cancellation response received. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[cancelInvoiceAutoReminder] Error cancelling invoice auto reminders:', error.message); + handleAxiosError(error); + } +} + export async function cancelSentInvoice( client: PayPalClient, context: Context, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index 6d5c940e..16cdd13e 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -232,6 +232,11 @@ export const setupInvoiceAutoReminderParameters = (context: Context) => ), }); +export const cancelInvoiceAutoReminderParameters = (context: Context) => + z.object({ + invoice_id: z.string().regex(INVOICE_ID_REGEX, "Invalid PayPal Invoice ID").describe('The ID of the invoice for which to cancel all scheduled automatic reminders.'), + }); + export const updateInvoiceAutoReminderParameters = (context: Context) => z.object({ reminder_configuration_id: z.string().describe('The ID of the auto reminder configuration to update.'), diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index 2bc7593a..732589b9 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -74,6 +74,12 @@ Update an existing invoice auto reminder configuration by its configuration ID. This function performs a full update of the reminder configuration's timing interval, repetition count and notification preferences. `; +export const cancelInvoiceAutoReminderPrompt = (context: Context) => ` +Cancel all scheduled automatic reminders for an invoice. + +This function permanently cancels every automatic reminder scheduled for a specific invoice, by invoice ID. This action is irreversible -- once cancelled, automatic reminders cannot be re-enabled for that invoice. +`; + export const generateInvoiceQrCodePrompt = (context: Context) => ` Generate a QR code for an invoice. diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index 709f0250..ea4a50a1 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -12,6 +12,7 @@ import { deleteInvoicePrompt, setupInvoiceAutoReminderPrompt, updateInvoiceAutoReminderPrompt, + cancelInvoiceAutoReminderPrompt, createShipmentPrompt, getShipmentTrackingPrompt, generateInvoiceQrCodePrompt, @@ -52,6 +53,7 @@ import { deleteInvoiceParameters, setupInvoiceAutoReminderParameters, updateInvoiceAutoReminderParameters, + cancelInvoiceAutoReminderParameters, createShipmentParameters, getShipmentTrackingParameters, generateInvoiceQrCodeParameters, @@ -216,6 +218,17 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'cancel_invoice_auto_reminder', + name: 'Cancel Invoice Auto Reminder', + description: cancelInvoiceAutoReminderPrompt(context), + parameters: cancelInvoiceAutoReminderParameters(context), + actions: { + invoices: { + cancelReminders: true, + }, + }, + }, { method: 'generate_invoice_qr_code', name: 'Generate Invoice QR Code', From c4bb2944a5ac96f53a2ca0110310cf8e66931da8 Mon Sep 17 00:00:00 2001 From: shrir-j Date: Mon, 10 Aug 2026 12:45:50 +0530 Subject: [PATCH 5/9] Add record payment for invoice tool --- .../shared/invoices/parameters.py | 23 ++++++++- .../shared/invoices/prompts.py | 8 +++ .../shared/invoices/tool_handlers.py | 15 ++++++ python/paypal_agent_toolkit/shared/tools.py | 11 ++++ typescript/src/shared/api.ts | 3 ++ typescript/src/shared/functions.ts | 50 ++++++++++++++++++- typescript/src/shared/parameters.ts | 14 +++++- typescript/src/shared/prompts.ts | 8 +++ typescript/src/shared/tools.ts | 13 +++++ 9 files changed, 142 insertions(+), 3 deletions(-) diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 7c369826..82218302 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, model_validator, field_validator from typing import List, Optional, Literal from ..regex import ( INVOICE_ID_REGEX, @@ -31,6 +31,11 @@ class Address(BaseModel): postal_code: Optional[str] = Field(None, description="The postal code, which is the zip code or equivalent.") country_code: Optional[str] = Field(None, pattern=COUNTRY_CODE_REGEX.pattern, description="The two-character ISO 3166-1 country code (for example, US or GB).") + @field_validator("country_code", mode="before") + @classmethod + def _empty_string_to_none(cls, v): + return None if v == "" else v + class Phone(BaseModel): country_code: str = Field(..., description="The country calling code, in E.164 format (for example, '1' for the United States).") @@ -235,3 +240,19 @@ class UpdateInvoiceAutoReminderParameters(BaseModel): notification: Optional[ReminderNotification] = Field(None, description="Notification settings for the reminder.") +class RecordPaymentForInvoiceParameters(BaseModel): + invoice_id: str = Field(..., pattern=INVOICE_ID_REGEX.pattern, description="The ID of the invoice to record the payment against.") + payment_id: Optional[str] = Field(None, max_length=22, description="The ID for a PayPal payment transaction. Required for the PAYPAL payment type.") + payment_date: Optional[str] = Field(None, pattern=DATE_NO_TIME_REGEX.pattern, description="The date when the invoicer recorded the payment, in yyyy-MM-dd format.") + payment_date_time: Optional[str] = Field(None, min_length=20, max_length=64, description="The date and time when the invoicer recorded the payment, in Internet date and time format (ISO 8601), for example 2018-05-13T21:20:00Z or 2018-05-13T21:20:00.000-08:00. Seconds are required.") + method: Literal["BANK_TRANSFER", "CASH", "CHECK", "CREDIT_CARD", "DEBIT_CARD", "PAYPAL", "WIRE_TRANSFER", "OTHER"] = Field(..., description="The payment mode or method through which the invoicer can accept the payments.") + note: Optional[str] = Field(None, max_length=2000, description="A note associated with an external cash or check payment.") + amount: Optional[Money] = Field(None, description="The currency and amount for a financial transaction.") + shipping_info: Optional[ShippingInfo] = Field(None, description="The shipping information associated with this payment.") + + @field_validator("payment_id", "payment_date", "payment_date_time", mode="before") + @classmethod + def _empty_string_to_none(cls, v): + return None if v == "" else v + + diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index f0a62b88..9455ff7d 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -68,4 +68,12 @@ Update an existing invoice auto reminder configuration by its configuration ID. This function performs a full update of the reminder configuration's timing interval, repetition count and notification preferences. +""" + +RECORD_PAYMENT_FOR_INVOICE_PROMPT = """ +Record a payment for an invoice on PayPal. + +This function records an external or manual payment (for example, cash, check, bank transfer, or a PayPal transaction) against an invoice, by invoice ID. If the recorded amount covers the full amount due, PayPal marks the invoice PAID; otherwise it is marked PARTIALLY_PAID. This does not process a new payment -- it only logs one that was already collected. + +method is required. payment_id applies only to PAYPAL-type payments. """ \ No newline at end of file diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index 624def9d..f5588f67 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -170,3 +170,18 @@ def update_invoice_auto_reminder(client, params: dict): response = client.put(uri=url, payload=payload, headers={"Prefer": "return=representation"}) return json.dumps(response) + + +def record_payment_for_invoice(client, params: dict): + + validated = RecordPaymentForInvoiceParameters(**params) + invoice_id = validated.invoice_id + payload = validated.model_dump(exclude_none=True, exclude={"invoice_id"}) + + url = f"/v2/invoicing/invoices/{invoice_id}/payments" + response = client.post(uri=url, payload=payload) + + if not response: + return json.dumps({"success": True, "invoice_id": invoice_id}) + + return json.dumps(response) diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index 0b5aea90..638fd8d1 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -27,6 +27,7 @@ GENERATE_INVOICE_QRCODE_PROMPT, SETUP_INVOICE_AUTO_REMINDER_PROMPT, UPDATE_INVOICE_AUTO_REMINDER_PROMPT, + RECORD_PAYMENT_FOR_INVOICE_PROMPT, ) from ..shared.disputes.prompts import ( @@ -80,6 +81,7 @@ GenerateInvoiceQrCodeParameters, SetupInvoiceAutoReminderParameters, UpdateInvoiceAutoReminderParameters, + RecordPaymentForInvoiceParameters, ) from ..shared.disputes.parameters import ( @@ -132,6 +134,7 @@ generate_invoice_qrcode, setup_invoice_auto_reminders, update_invoice_auto_reminder, + record_payment_for_invoice, ) @@ -342,6 +345,14 @@ "actions": {"invoices": {"updateReminder": True}}, "execute": update_invoice_auto_reminder, }, + { + "method": "record_payment_for_invoice", + "name": "Record Payment For Invoice", + "description": RECORD_PAYMENT_FOR_INVOICE_PROMPT.strip(), + "args_schema": RecordPaymentForInvoiceParameters, + "actions": {"invoices": {"recordPayment": True}}, + "execute": record_payment_for_invoice, + }, { "method": "list_disputes", "name": "List Disputes", diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index c59bb0eb..6e54db40 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -17,6 +17,7 @@ import { getShipmentTracking, updateShipmentTracking, generateInvoiceQrCode, + recordPaymentForInvoice, createOrder, getOrder, listDisputes, @@ -111,6 +112,8 @@ class PayPalAPI { return updateInvoiceAutoReminder(this.paypalClient, this.context, arg); case 'generate_invoice_qr_code': return generateInvoiceQrCode(this.paypalClient, this.context, arg); + case 'record_payment_for_invoice': + return recordPaymentForInvoice(this.paypalClient, this.context, arg); case 'create_product': return createProduct(this.paypalClient, this.context, arg); case 'list_products': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index e968814e..5bc91d53 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -35,7 +35,8 @@ import { updatePlanParameters, getMerchantInsightsParameters, setupInvoiceAutoReminderParameters, - updateInvoiceAutoReminderParameters + updateInvoiceAutoReminderParameters, + recordPaymentForInvoiceParameters } from "./parameters"; import {parseOrderDetails, parseUpdateSubscriptionPayload, buildCreateInvoicePayload, buildCreateRecurringSeriesPayload, toQueryString} from "./payloadUtils"; import { TypeOf } from "zod"; @@ -386,6 +387,53 @@ export async function generateInvoiceQrCode( } } +export async function recordPaymentForInvoice( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[recordPaymentForInvoice] Starting to record payment for invoice'); + const { + invoice_id, + payment_id, + payment_date, + payment_date_time, + method, + note, + amount, + shipping_info, + } = params; + + const body = { + payment_id, + payment_date, + payment_date_time, + method, + note, + amount, + shipping_info, + }; + + const headers = await client.getHeaders(); + logger('[recordPaymentForInvoice] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/invoices/${invoice_id}/payments`; + + try { + logger('[recordPaymentForInvoice] Sending request to PayPal API'); + const response = await axios.post(url, body, { headers }); + if (response.status === 204) { + logger(`[recordPaymentForInvoice] Payment recorded successfully. Status: ${response.status}`); + return { success: true, invoice_id }; + } + logger(`[recordPaymentForInvoice] Payment record response received. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[recordPaymentForInvoice] Error recording payment for invoice:', error.message); + handleAxiosError(error); + } +} + // === PRODUCT FUNCTIONS === export async function createProduct( client: PayPalClient, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index cffef739..dbe38afc 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -31,7 +31,7 @@ const address = () => z.object({ admin_area_2: z.string().optional().describe("A city, town, or village."), admin_area_1: z.string().optional().describe("The highest-level sub-division in a country, such as a state or province."), postal_code: z.string().optional().describe("The postal code, which is the zip code or equivalent."), - country_code: z.string().regex(COUNTRY_CODE_REGEX, "country_code must be a two-character ISO 3166-1 country code").optional().describe("The two-character ISO 3166-1 country code (for example, US or GB)."), + country_code: z.preprocess((val) => (val === '' ? undefined : val), z.string().regex(COUNTRY_CODE_REGEX, "country_code must be a two-character ISO 3166-1 country code").optional()).describe("The two-character ISO 3166-1 country code (for example, US or GB)."), }).describe("address object"); const phone = () => z.object({ @@ -242,6 +242,18 @@ export const updateInvoiceAutoReminderParameters = (context: Context) => }).optional().describe('Notification settings for the reminder.'), }).describe('Full replacement configuration for an existing invoice auto reminder. All required fields must be included since this performs a full update.'); +export const recordPaymentForInvoiceParameters = (context: Context) => + z.object({ + invoice_id: z.string().regex(INVOICE_ID_REGEX, "Invalid PayPal Invoice ID").describe('The ID of the invoice to record the payment against.'), + payment_id: z.preprocess((val) => (val === '' ? undefined : val), z.string().max(22).optional()).describe('The ID for a PayPal payment transaction. Required for the PAYPAL payment type.'), + payment_date: z.preprocess((val) => (val === '' ? undefined : val), z.string().regex(DATE_NO_TIME_REGEX, "payment_date must be in yyyy-MM-DD format").optional()).describe('The date when the invoicer recorded the payment, in yyyy-MM-dd format.'), + payment_date_time: z.preprocess((val) => (val === '' ? undefined : val), z.string().min(20).max(64).optional()).describe('The date and time when the invoicer recorded the payment, in Internet date and time format (ISO 8601), for example 2018-05-13T21:20:00Z or 2018-05-13T21:20:00.000-08:00. Seconds are required.'), + method: z.enum(['BANK_TRANSFER', 'CASH', 'CHECK', 'CREDIT_CARD', 'DEBIT_CARD', 'PAYPAL', 'WIRE_TRANSFER', 'OTHER']).describe('The payment mode or method through which the invoicer can accept the payments.'), + note: z.string().max(2000).optional().describe('A note associated with an external cash or check payment.'), + amount: money().optional().describe('The currency and amount for a financial transaction.'), + shipping_info: shippingInfo().optional().describe('The shipping information associated with this payment.'), + }).describe('Record an external or PayPal payment against an invoice.'); + export const updateProductParameters = (context: Context) => z.object({ diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index 23fdb2a4..8aa8bf79 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -74,6 +74,14 @@ Generate a QR code for an invoice. This function generates a QR code for an invoice, which can be used to pay the invoice using a mobile device or scanning app. `; +export const recordPaymentForInvoicePrompt = (context: Context) => ` +Record a payment for an invoice on PayPal. + +This function records an external or manual payment (for example, cash, check, bank transfer, or a PayPal transaction) against an invoice, by invoice ID. If the recorded amount covers the full amount due, PayPal marks the invoice PAID; otherwise it is marked PARTIALLY_PAID. This does not process a new payment -- it only logs one that was already collected. + +method is required. payment_id applies only to PAYPAL-type payments. +`; + export const createProductPrompt = (context: Context) => ` Create a product in PayPal using product catalog - create products API. This function creates a new product that will be used in subscription plans, subscriptions. diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index c7b1fee7..899bddae 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -14,6 +14,7 @@ import { createShipmentPrompt, getShipmentTrackingPrompt, generateInvoiceQrCodePrompt, + recordPaymentForInvoicePrompt, createOrderPrompt, getOrderPrompt, updateShipmentTrackingPrompt, @@ -53,6 +54,7 @@ import { createShipmentParameters, getShipmentTrackingParameters, generateInvoiceQrCodeParameters, + recordPaymentForInvoiceParameters, createOrderParameters, getOrderParameters, updateShipmentTrackingParameters, @@ -214,6 +216,17 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'record_payment_for_invoice', + name: 'Record Payment For Invoice', + description: recordPaymentForInvoicePrompt(context), + parameters: recordPaymentForInvoiceParameters(context), + actions: { + invoices: { + recordPayment: true, + }, + }, + }, { method: 'create_product', name: 'Create Product', From df3680dd07ddf4af881fece09a88101c4823da31 Mon Sep 17 00:00:00 2001 From: shrir-j Date: Mon, 10 Aug 2026 15:03:47 +0530 Subject: [PATCH 6/9] Add record refund for invoice tool --- .../shared/invoices/parameters.py | 12 ++++++ .../shared/invoices/prompts.py | 8 ++++ .../shared/invoices/tool_handlers.py | 15 +++++++ python/paypal_agent_toolkit/shared/tools.py | 11 +++++ typescript/src/shared/api.ts | 3 ++ typescript/src/shared/functions.ts | 42 ++++++++++++++++++- typescript/src/shared/parameters.ts | 8 ++++ typescript/src/shared/prompts.ts | 8 ++++ typescript/src/shared/tools.ts | 13 ++++++ 9 files changed, 119 insertions(+), 1 deletion(-) diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 82218302..6deab2d5 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -256,3 +256,15 @@ def _empty_string_to_none(cls, v): return None if v == "" else v +class RecordRefundForInvoiceParameters(BaseModel): + invoice_id: str = Field(..., pattern=INVOICE_ID_REGEX.pattern, description="The ID of the invoice to mark as refunded.") + refund_date: Optional[str] = Field(None, pattern=DATE_NO_TIME_REGEX.pattern, description="The date when the invoicer recorded the refund, in yyyy-MM-dd format.") + amount: Optional[Money] = Field(None, description="The currency and amount for a financial transaction.") + method: Literal["BANK_TRANSFER", "CASH", "CHECK", "CREDIT_CARD", "DEBIT_CARD", "PAYPAL", "WIRE_TRANSFER", "OTHER"] = Field(..., description="The payment mode or method through which the invoicer can accept the payments.") + + @field_validator("refund_date", mode="before") + @classmethod + def _empty_string_to_none(cls, v): + return None if v == "" else v + + diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index 9455ff7d..d18ad102 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -76,4 +76,12 @@ This function records an external or manual payment (for example, cash, check, bank transfer, or a PayPal transaction) against an invoice, by invoice ID. If the recorded amount covers the full amount due, PayPal marks the invoice PAID; otherwise it is marked PARTIALLY_PAID. This does not process a new payment -- it only logs one that was already collected. method is required. payment_id applies only to PAYPAL-type payments. +""" + +RECORD_REFUND_FOR_INVOICE_PROMPT = """ +Record a refund for an invoice on PayPal. + +This function records a refund against an invoice, by invoice ID. If all payments on the invoice are refunded, PayPal marks the invoice REFUNDED; otherwise it is marked PARTIALLY_REFUNDED. This does not process a new refund -- it only logs one that was already issued. + +method is required. """ \ No newline at end of file diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index f5588f67..4a1aaa3a 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -185,3 +185,18 @@ def record_payment_for_invoice(client, params: dict): return json.dumps({"success": True, "invoice_id": invoice_id}) return json.dumps(response) + + +def record_refund_for_invoice(client, params: dict): + + validated = RecordRefundForInvoiceParameters(**params) + invoice_id = validated.invoice_id + payload = validated.model_dump(exclude_none=True, exclude={"invoice_id"}) + + url = f"/v2/invoicing/invoices/{invoice_id}/refunds" + response = client.post(uri=url, payload=payload) + + if not response: + return json.dumps({"success": True, "invoice_id": invoice_id}) + + return json.dumps(response) diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index 638fd8d1..2ef8ff7a 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -28,6 +28,7 @@ SETUP_INVOICE_AUTO_REMINDER_PROMPT, UPDATE_INVOICE_AUTO_REMINDER_PROMPT, RECORD_PAYMENT_FOR_INVOICE_PROMPT, + RECORD_REFUND_FOR_INVOICE_PROMPT, ) from ..shared.disputes.prompts import ( @@ -82,6 +83,7 @@ SetupInvoiceAutoReminderParameters, UpdateInvoiceAutoReminderParameters, RecordPaymentForInvoiceParameters, + RecordRefundForInvoiceParameters, ) from ..shared.disputes.parameters import ( @@ -135,6 +137,7 @@ setup_invoice_auto_reminders, update_invoice_auto_reminder, record_payment_for_invoice, + record_refund_for_invoice, ) @@ -353,6 +356,14 @@ "actions": {"invoices": {"recordPayment": True}}, "execute": record_payment_for_invoice, }, + { + "method": "record_refund_for_invoice", + "name": "Record Refund For Invoice", + "description": RECORD_REFUND_FOR_INVOICE_PROMPT.strip(), + "args_schema": RecordRefundForInvoiceParameters, + "actions": {"invoices": {"recordRefund": True}}, + "execute": record_refund_for_invoice, + }, { "method": "list_disputes", "name": "List Disputes", diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index 6e54db40..81f70355 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -18,6 +18,7 @@ import { updateShipmentTracking, generateInvoiceQrCode, recordPaymentForInvoice, + recordRefundForInvoice, createOrder, getOrder, listDisputes, @@ -114,6 +115,8 @@ class PayPalAPI { return generateInvoiceQrCode(this.paypalClient, this.context, arg); case 'record_payment_for_invoice': return recordPaymentForInvoice(this.paypalClient, this.context, arg); + case 'record_refund_for_invoice': + return recordRefundForInvoice(this.paypalClient, this.context, arg); case 'create_product': return createProduct(this.paypalClient, this.context, arg); case 'list_products': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index 5bc91d53..4ff1fce6 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -36,7 +36,8 @@ import { getMerchantInsightsParameters, setupInvoiceAutoReminderParameters, updateInvoiceAutoReminderParameters, - recordPaymentForInvoiceParameters + recordPaymentForInvoiceParameters, + recordRefundForInvoiceParameters } from "./parameters"; import {parseOrderDetails, parseUpdateSubscriptionPayload, buildCreateInvoicePayload, buildCreateRecurringSeriesPayload, toQueryString} from "./payloadUtils"; import { TypeOf } from "zod"; @@ -434,6 +435,45 @@ export async function recordPaymentForInvoice( } } +export async function recordRefundForInvoice( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[recordRefundForInvoice] Starting to record refund for invoice'); + const { + invoice_id, + refund_date, + amount, + method, + } = params; + + const body = { + refund_date, + amount, + method, + }; + + const headers = await client.getHeaders(); + logger('[recordRefundForInvoice] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/invoices/${invoice_id}/refunds`; + + try { + logger('[recordRefundForInvoice] Sending request to PayPal API'); + const response = await axios.post(url, body, { headers }); + if (response.status === 204) { + logger(`[recordRefundForInvoice] Refund recorded successfully. Status: ${response.status}`); + return { success: true, invoice_id }; + } + logger(`[recordRefundForInvoice] Refund record response received. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[recordRefundForInvoice] Error recording refund for invoice:', error.message); + handleAxiosError(error); + } +} + // === PRODUCT FUNCTIONS === export async function createProduct( client: PayPalClient, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index dbe38afc..64a08959 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -254,6 +254,14 @@ export const recordPaymentForInvoiceParameters = (context: Context) => shipping_info: shippingInfo().optional().describe('The shipping information associated with this payment.'), }).describe('Record an external or PayPal payment against an invoice.'); +export const recordRefundForInvoiceParameters = (context: Context) => + z.object({ + invoice_id: z.string().regex(INVOICE_ID_REGEX, "Invalid PayPal Invoice ID").describe('The ID of the invoice to mark as refunded.'), + refund_date: z.preprocess((val) => (val === '' ? undefined : val), z.string().regex(DATE_NO_TIME_REGEX, "refund_date must be in yyyy-MM-DD format").optional()).describe('The date when the invoicer recorded the refund, in yyyy-MM-dd format.'), + amount: money().optional().describe('The currency and amount for a financial transaction.'), + method: z.enum(['BANK_TRANSFER', 'CASH', 'CHECK', 'CREDIT_CARD', 'DEBIT_CARD', 'PAYPAL', 'WIRE_TRANSFER', 'OTHER']).describe('The payment mode or method through which the invoicer can accept the payments.'), + }).describe('Record a refund against an invoice.'); + export const updateProductParameters = (context: Context) => z.object({ diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index 8aa8bf79..276c944b 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -82,6 +82,14 @@ This function records an external or manual payment (for example, cash, check, b method is required. payment_id applies only to PAYPAL-type payments. `; +export const recordRefundForInvoicePrompt = (context: Context) => ` +Record a refund for an invoice on PayPal. + +This function records a refund against an invoice, by invoice ID. If all payments made against the invoice are refunded, PayPal marks the invoice REFUNDED; otherwise it is marked PARTIALLY_REFUNDED. This does not process a new refund -- it only logs one that was already issued. + +method is required. +`; + export const createProductPrompt = (context: Context) => ` Create a product in PayPal using product catalog - create products API. This function creates a new product that will be used in subscription plans, subscriptions. diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index 899bddae..09393b08 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -15,6 +15,7 @@ import { getShipmentTrackingPrompt, generateInvoiceQrCodePrompt, recordPaymentForInvoicePrompt, + recordRefundForInvoicePrompt, createOrderPrompt, getOrderPrompt, updateShipmentTrackingPrompt, @@ -55,6 +56,7 @@ import { getShipmentTrackingParameters, generateInvoiceQrCodeParameters, recordPaymentForInvoiceParameters, + recordRefundForInvoiceParameters, createOrderParameters, getOrderParameters, updateShipmentTrackingParameters, @@ -227,6 +229,17 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'record_refund_for_invoice', + name: 'Record Refund For Invoice', + description: recordRefundForInvoicePrompt(context), + parameters: recordRefundForInvoiceParameters(context), + actions: { + invoices: { + recordRefund: true, + }, + }, + }, { method: 'create_product', name: 'Create Product', From 19fa3bf8e379de51aac9825a3c556fcfcbc8184f Mon Sep 17 00:00:00 2001 From: shrir-j Date: Tue, 11 Aug 2026 10:16:47 +0530 Subject: [PATCH 7/9] Add update invoice and recurring tools --- .../shared/invoices/parameters.py | 58 ++++++++++++ .../shared/invoices/prompts.py | 8 +- .../shared/invoices/tool_handlers.py | 37 ++++++++ .../shared/paypal_client.py | 12 ++- python/paypal_agent_toolkit/shared/tools.py | 11 +++ typescript/src/shared/api.ts | 3 + typescript/src/shared/functions.ts | 94 ++++++++++++++++++- typescript/src/shared/parameters.ts | 28 ++++++ typescript/src/shared/prompts.ts | 6 ++ typescript/src/shared/tools.ts | 13 +++ 10 files changed, 263 insertions(+), 7 deletions(-) diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 7c369826..4a2675fe 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -235,3 +235,61 @@ class UpdateInvoiceAutoReminderParameters(BaseModel): notification: Optional[ReminderNotification] = Field(None, description="Notification settings for the reminder.") +# ---- update_invoicing: one external tool that internally branches to an invoice-update flow ---- +# ---- or a recurring-series-update flow based on resource_type. invoice_update/recurring_series_update ---- +# ---- are full-replacement bodies -- the create models extended with the resource's ID (and, for ---- +# ---- invoices, the two query-param booleans) -- since PayPal's update endpoints are full-body PUTs. ---- + +def _is_empty_update(value): + if value is None: + return True + if isinstance(value, BaseModel): + return _is_empty_update(value.model_dump()) + if isinstance(value, str): + return not value.strip() + if isinstance(value, dict): + return all(_is_empty_update(v) for v in value.values()) + if isinstance(value, list): + return len(value) == 0 + return False + + +class UpdateInvoiceBody(CreateInvoiceParameters): + # Optional at the field level (even though required whenever this flow actually runs) so that a + # client blanking out the unused side of update_invoicing instead of omitting it can still pass + # validation -- the pattern would otherwise reject an empty placeholder before the dispatcher's + # own "is this side really being used" check ever runs. Presence is enforced in the validator below. + invoice_id: Optional[str] = Field(None, pattern=INVOICE_ID_REGEX.pattern, description="The ID of the invoice to update. Required when resource_type is 'invoice'.") + send_to_recipient: Optional[bool] = Field(None, description="Whether to send the invoice update notification to the recipient. PayPal defaults to true if omitted.") + send_to_invoicer: Optional[bool] = Field(None, description="Whether to send the invoice update notification to the merchant (invoicer). PayPal defaults to true if omitted.") + + +class UpdateRecurringSeriesBody(CreateRecurringSeriesParameters): + # Optional for the same reason as invoice_id above; enforced in the validator below. + recurring_series_id: Optional[str] = Field(None, pattern=RECURRING_SERIES_ID_REGEX.pattern, description="The ID of the recurring invoice series to update. Required when resource_type is 'recurring_series'.") + + +class UpdateInvoicingParameters(BaseModel): + resource_type: Literal["invoice", "recurring_series"] = Field(..., description="Which kind of resource to update. 'invoice' updates an individual invoice; 'recurring_series' updates a recurring invoice series.") + invoice_update: Optional[UpdateInvoiceBody] = Field(None, description="Full replacement content for the invoice. Set only when resource_type is 'invoice'.") + recurring_series_update: Optional[UpdateRecurringSeriesBody] = Field(None, description="Full replacement content for the recurring series. Set only when resource_type is 'recurring_series'.") + + @model_validator(mode="after") + def _check_update_matches_resource_type(self): + if self.resource_type == "invoice": + if self.invoice_update is None: + raise ValueError("invoice_update is required when resource_type is 'invoice'.") + if not self.invoice_update.invoice_id: + raise ValueError("invoice_update.invoice_id is required when resource_type is 'invoice'.") + if not _is_empty_update(self.recurring_series_update): + raise ValueError("recurring_series_update cannot be set when resource_type is 'invoice' -- use invoice_update instead.") + else: + if self.recurring_series_update is None: + raise ValueError("recurring_series_update is required when resource_type is 'recurring_series'.") + if not self.recurring_series_update.recurring_series_id: + raise ValueError("recurring_series_update.recurring_series_id is required when resource_type is 'recurring_series'.") + if not _is_empty_update(self.invoice_update): + raise ValueError("invoice_update cannot be set when resource_type is 'recurring_series' -- use recurring_series_update instead.") + return self + + diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index f0a62b88..70759faa 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -68,4 +68,10 @@ Update an existing invoice auto reminder configuration by its configuration ID. This function performs a full update of the reminder configuration's timing interval, repetition count and notification preferences. -""" \ No newline at end of file +""" + +UPDATE_INVOICING_PROMPT = """ +Update an existing invoice or recurring invoice series on PayPal. + +For invoices, the recipient (primary_recipients) can only be changed 2 times within any 72-hour window -- avoid unnecessary recipient edits. +""" diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index 624def9d..46ef3c9e 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -170,3 +170,40 @@ def update_invoice_auto_reminder(client, params: dict): response = client.put(uri=url, payload=payload, headers={"Prefer": "return=representation"}) return json.dumps(response) + + +def _update_invoice(client, body: UpdateInvoiceBody): + + payload = build_create_invoice_payload( + body.model_dump(exclude_none=True, exclude={"invoice_id", "send_to_recipient", "send_to_invoicer"}) + ) + query = "&".join( + f"{k}={str(v).lower()}" + for k, v in [("send_to_recipient", body.send_to_recipient), ("send_to_invoicer", body.send_to_invoicer)] + if v is not None + ) + url = f"/v2/invoicing/invoices/{body.invoice_id}" + (f"?{query}" if query else "") + response = client.put(uri=url, payload=payload, headers={"Prefer": "return=representation"}) + + return json.dumps(response) + + +def _update_recurring_series(client, body: UpdateRecurringSeriesBody): + + payload = build_create_recurring_series_payload( + body.model_dump(exclude_none=True, exclude={"recurring_series_id"}) + ) + url = f"/v2/invoicing/recurring-invoices/{body.recurring_series_id}" + response = client.put(uri=url, payload=payload, headers={"Prefer": "return=representation"}) + + return json.dumps(response) + + +def update_invoicing(client, params: dict): + + validated = UpdateInvoicingParameters(**params) + + if validated.resource_type == "invoice": + return _update_invoice(client, validated.invoice_update) + return _update_recurring_series(client, validated.recurring_series_update) + diff --git a/python/paypal_agent_toolkit/shared/paypal_client.py b/python/paypal_agent_toolkit/shared/paypal_client.py index 93d9145d..0be8208e 100644 --- a/python/paypal_agent_toolkit/shared/paypal_client.py +++ b/python/paypal_agent_toolkit/shared/paypal_client.py @@ -117,14 +117,16 @@ def get(self, uri): return json_response - def put(self, uri, payload): - + def put(self, uri, payload, headers=None): + url = f"{self.base_url}{uri}" - headers = self.build_headers() - logRequestPayload(payload, url, headers) + request_headers = self.build_headers() + if headers: + request_headers.update(headers) + logRequestPayload(payload, url, request_headers) try: - response = requests.put(url, headers=headers, json=payload) + response = requests.put(url, headers=request_headers, json=payload) response.raise_for_status() except requests.exceptions.RequestException as e: self.log_request_exception(e, url) diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index 0b5aea90..7fb40174 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -27,6 +27,7 @@ GENERATE_INVOICE_QRCODE_PROMPT, SETUP_INVOICE_AUTO_REMINDER_PROMPT, UPDATE_INVOICE_AUTO_REMINDER_PROMPT, + UPDATE_INVOICING_PROMPT, ) from ..shared.disputes.prompts import ( @@ -80,6 +81,7 @@ GenerateInvoiceQrCodeParameters, SetupInvoiceAutoReminderParameters, UpdateInvoiceAutoReminderParameters, + UpdateInvoicingParameters, ) from ..shared.disputes.parameters import ( @@ -132,6 +134,7 @@ generate_invoice_qrcode, setup_invoice_auto_reminders, update_invoice_auto_reminder, + update_invoicing, ) @@ -342,6 +345,14 @@ "actions": {"invoices": {"updateReminder": True}}, "execute": update_invoice_auto_reminder, }, + { + "method": "update_invoicing", + "name": "Update Invoice or Recurring Invoice Series", + "description": UPDATE_INVOICING_PROMPT.strip(), + "args_schema": UpdateInvoicingParameters, + "actions": {"invoices": {"update": True}}, + "execute": update_invoicing, + }, { "method": "list_disputes", "name": "List Disputes", diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index c59bb0eb..9968a6b3 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -9,6 +9,7 @@ import { cancelSentInvoice, setupInvoiceAutoReminder, updateInvoiceAutoReminder, + updateInvoicing, createProduct, listProducts, createSubscriptionPlan, @@ -109,6 +110,8 @@ class PayPalAPI { return setupInvoiceAutoReminder(this.paypalClient, this.context, arg); case 'update_invoice_auto_reminder': return updateInvoiceAutoReminder(this.paypalClient, this.context, arg); + case 'update_invoicing': + return updateInvoicing(this.paypalClient, this.context, arg); case 'generate_invoice_qr_code': return generateInvoiceQrCode(this.paypalClient, this.context, arg); case 'create_product': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index e968814e..b1112585 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -35,7 +35,10 @@ import { updatePlanParameters, getMerchantInsightsParameters, setupInvoiceAutoReminderParameters, - updateInvoiceAutoReminderParameters + updateInvoiceAutoReminderParameters, + updateInvoicingParameters, + updateInvoiceBodyParameters, + updateRecurringSeriesBodyParameters } from "./parameters"; import {parseOrderDetails, parseUpdateSubscriptionPayload, buildCreateInvoicePayload, buildCreateRecurringSeriesPayload, toQueryString} from "./payloadUtils"; import { TypeOf } from "zod"; @@ -155,6 +158,95 @@ export async function activateRecurringSeries( } } +// Treats a blanked-out update object (all-undefined fields) as equivalent to "not provided", +// so clients that clear the unused side of update_invoicing by blanking it instead of omitting +// it don't trip the mutual-exclusivity check below. +function isEmptyFilters(value: any): boolean { + if (value === undefined || value === null) return true; + if (typeof value === 'string') return value.trim().length === 0; + if (Array.isArray(value)) return value.length === 0; + if (typeof value === 'object') return Object.values(value).every(isEmptyFilters); + return false; +} + +async function updateInvoice( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[updateInvoice] Starting to update invoice'); + const headers = await client.getHeaders(); + const { invoice_id, send_to_recipient, send_to_invoicer } = params; + const url = `${client.getBaseUrl()}/v2/invoicing/invoices/${invoice_id}`; + + try { + logger('[updateInvoice] Sending request to PayPal API'); + const invoicePayload = buildCreateInvoicePayload(params); + const response = await axios.put(url, invoicePayload, { + headers: { ...headers, Prefer: 'return=representation' }, + params: { send_to_recipient, send_to_invoicer }, + }); + logger(`[updateInvoice] Invoice updated successfully. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[updateInvoice] Error updating invoice:', error.message); + handleAxiosError(error); + } +} + +async function updateRecurringSeries( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[updateRecurringSeries] Starting to update recurring series'); + const headers = await client.getHeaders(); + const { recurring_series_id } = params; + const url = `${client.getBaseUrl()}/v2/invoicing/recurring-invoices/${recurring_series_id}`; + + try { + logger('[updateRecurringSeries] Sending request to PayPal API'); + const recurringSeriesPayload = buildCreateRecurringSeriesPayload(params); + const response = await axios.put(url, recurringSeriesPayload, { + headers: { ...headers, Prefer: 'return=representation' }, + }); + logger(`[updateRecurringSeries] Recurring series updated successfully. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[updateRecurringSeries] Error updating recurring series:', error.message); + handleAxiosError(error); + } +} + +export async function updateInvoicing( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + if (params.resource_type === 'invoice') { + if (!isEmptyFilters(params.recurring_series_update)) { + throw new Error("recurring_series_update cannot be set when resource_type is 'invoice' -- use invoice_update instead."); + } + if (!params.invoice_update) { + throw new Error("invoice_update is required when resource_type is 'invoice'."); + } + if (!params.invoice_update.invoice_id) { + throw new Error("invoice_update.invoice_id is required when resource_type is 'invoice'."); + } + return updateInvoice(client, context, params.invoice_update as TypeOf> & { invoice_id: string }); + } + if (!isEmptyFilters(params.invoice_update)) { + throw new Error("invoice_update cannot be set when resource_type is 'recurring_series' -- use recurring_series_update instead."); + } + if (!params.recurring_series_update) { + throw new Error("recurring_series_update is required when resource_type is 'recurring_series'."); + } + if (!params.recurring_series_update.recurring_series_id) { + throw new Error("recurring_series_update.recurring_series_id is required when resource_type is 'recurring_series'."); + } + return updateRecurringSeries(client, context, params.recurring_series_update as TypeOf> & { recurring_series_id: string }); +} + export async function listInvoices( client: PayPalClient, context: Context, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index cffef739..cb454325 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -157,6 +157,34 @@ export const createRecurringSeriesParameters = (context: Context) => z.object({ minimum_partial_payment_amount: z.string().regex(DECIMAL_STRING_REGEX, "minimum_partial_payment_amount must be a numeric value, e.g. \"20.00\"").optional().describe("The minimum amount allowed for a partial payment on each generated invoice, in the series' currency_code. Valid only when allow_partial_payment is true."), }).describe("Simplified create-recurring-series request. The tool implementation builds PayPal's actual nested recurring-invoicing API request from these flat fields."); +// update_invoicing is one external tool that internally branches to an invoice-update flow +// or a recurring-series-update flow based on resource_type. invoice_update/recurring_series_update +// are full-replacement bodies -- the create schemas extended with the resource's ID (and, for +// invoices, the two query-param booleans) -- since PayPal's update endpoints are full-body PUTs. + +export const updateInvoiceBodyParameters = (context: Context) => + createInvoiceParameters(context).extend({ + // Optional at the schema level (even though it's required whenever this flow actually runs) so that + // a client blanking out the unused side of update_invoicing instead of omitting it can still pass + // validation -- the regex would otherwise reject an empty placeholder before the dispatcher's own + // "is this side really being used" check ever runs. Presence is enforced at runtime in updateInvoicing. + invoice_id: z.string().regex(INVOICE_ID_REGEX, "Invalid PayPal Invoice ID").optional().describe("The ID of the invoice to update. Required when resource_type is 'invoice'."), + send_to_recipient: z.boolean().optional().describe("Whether to send the invoice update notification to the recipient. PayPal defaults to true if omitted."), + send_to_invoicer: z.boolean().optional().describe("Whether to send the invoice update notification to the merchant (invoicer). PayPal defaults to true if omitted."), + }); + +export const updateRecurringSeriesBodyParameters = (context: Context) => + createRecurringSeriesParameters(context).extend({ + // Optional for the same reason as invoice_id above; enforced at runtime in updateInvoicing. + recurring_series_id: z.string().regex(RECURRING_SERIES_ID_REGEX, "Invalid PayPal Recurring Series ID").optional().describe("The ID of the recurring invoice series to update. Required when resource_type is 'recurring_series'."), + }); + +export const updateInvoicingParameters = (context: Context) => z.object({ + resource_type: z.enum(["invoice", "recurring_series"]).describe("Which kind of resource to update. 'invoice' updates an individual invoice; 'recurring_series' updates a recurring invoice series."), + invoice_update: updateInvoiceBodyParameters(context).optional().describe("Full replacement content for the invoice. Set only when resource_type is 'invoice'."), + recurring_series_update: updateRecurringSeriesBodyParameters(context).optional().describe("Full replacement content for the recurring series. Set only when resource_type is 'recurring_series'."), +}).describe("Update an existing invoice or recurring invoice series on PayPal, depending on resource_type. This is a full-replacement update -- resend the complete content, not just changed fields."); + export const activateRecurringSeriesParameters = (context: Context) => z.object({ recurring_series_id: z.string() .regex(RECURRING_SERIES_ID_REGEX, "Invalid PayPal Recurring Series ID") diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index 23fdb2a4..18f54f62 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -20,6 +20,12 @@ primary_recipients and items use PayPal's real nested shape (billing_info/shippi A newly created series is in DRAFT status and will not generate invoices until activated -- call activate_recurring_series with the returned series ID to activate it. `; +export const updateInvoicingPrompt = (context: Context) => ` +Update an existing invoice or recurring invoice series on PayPal. + +For invoices, the recipient (primary_recipients) can only be changed 2 times within any 72-hour window -- avoid unnecessary recipient edits. +`; + export const activateRecurringSeriesPrompt = (context: Context) => ` Activate a recurring invoice series on PayPal. diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index c7b1fee7..4a104929 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -11,6 +11,7 @@ import { cancelSentInvoicePrompt, setupInvoiceAutoReminderPrompt, updateInvoiceAutoReminderPrompt, + updateInvoicingPrompt, createShipmentPrompt, getShipmentTrackingPrompt, generateInvoiceQrCodePrompt, @@ -50,6 +51,7 @@ import { cancelSentInvoiceParameters, setupInvoiceAutoReminderParameters, updateInvoiceAutoReminderParameters, + updateInvoicingParameters, createShipmentParameters, getShipmentTrackingParameters, generateInvoiceQrCodeParameters, @@ -203,6 +205,17 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'update_invoicing', + name: 'Update Invoice or Recurring Invoice Series', + description: updateInvoicingPrompt(context), + parameters: updateInvoicingParameters(context), + actions: { + invoices: { + update: true, + }, + }, + }, { method: 'generate_invoice_qr_code', name: 'Generate Invoice QR Code', From 3e9d222c2c46b81e623db73c0440fffd56dbd579 Mon Sep 17 00:00:00 2001 From: shrir-j Date: Tue, 11 Aug 2026 12:37:07 +0530 Subject: [PATCH 8/9] Add create conditional invoice tool --- PROMPTS.md | 94 +++++++++++++++++++ README.md | 16 ++++ python/README.md | 8 ++ .../shared/invoices/parameters.py | 32 +++++++ .../shared/invoices/prompts.py | 4 + .../shared/invoices/tool_handlers.py | 15 +++ python/paypal_agent_toolkit/shared/tools.py | 11 +++ typescript/README.md | 16 ++++ typescript/src/shared/api.ts | 3 + typescript/src/shared/functions.ts | 31 ++++++ typescript/src/shared/parameters.ts | 28 ++++++ typescript/src/shared/prompts.ts | 4 + typescript/src/shared/tools.ts | 13 +++ 13 files changed, 275 insertions(+) diff --git a/PROMPTS.md b/PROMPTS.md index 4583b41a..75384224 100644 --- a/PROMPTS.md +++ b/PROMPTS.md @@ -75,6 +75,100 @@ The PayPal Agent toolkit provides the following tools: > **Example Prompt**: > Generate a QR code for invoice {invoice_id} +--- + +**`delete_invoice`** - Permanently deletes a draft or scheduled invoice. Does not work on invoices that have already been sent -- use `cancel_sent_invoice` for those instead. + +- `invoice_id` (string, required): The ID of the draft or scheduled invoice to delete. + +> **Example Prompt**: +> Delete invoice {invoice_id} + +--- + +**`generate_invoice_number`** - Generates the next invoice number available to the merchant, based on the prefix/suffix and numeric portion of their last invoice number. + +- No parameters required. + +> **Example Prompt**: +> Generate the next invoice number for me + +--- + +**`search_invoicing`** - Searches for invoices or recurring invoice series. Set `resource_type` to `"invoice"` with `invoice_filters`, or `"recurring_series"` with `recurring_series_filters` -- only set the matching filters object. + +- `resource_type` (string, required): `"invoice"` or `"recurring_series"`. +- `page` (number, optional): The page number of the result set to fetch. Defaults to 1. +- `page_size` (number, optional): The number of records to return per page (max 100). Defaults to 20. +- `total_required` (boolean, optional): Whether to include total_pages/total_items. Only applies to invoice search. +- `invoice_filters` (object, optional): Filters such as `recipient_email`, `status`, `invoice_number`, `total_amount_range`, `invoice_date_range`. Set only when `resource_type` is `"invoice"`. +- `recurring_series_filters` (object, optional): Filters such as `search_text`, `search_fields`, and structured `search_filters` (status, currency_code, date ranges). Set only when `resource_type` is `"recurring_series"`. Covers only the past 3 years. + +> **Example Prompt**: +> Search for all PAID invoices for recipient {recipient_email} +> Search for active recurring series with a total amount between {lower_amount} and {upper_amount} + +--- + +**`update_invoicing`** - Updates an existing invoice or recurring invoice series. This is a full-replacement update -- resend the complete invoice/series content, not just the changed fields. Set `resource_type` to `"invoice"` with `invoice_update`, or `"recurring_series"` with `recurring_series_update` -- only set the matching object. + +- `resource_type` (string, required): `"invoice"` or `"recurring_series"`. +- `invoice_update` (object, optional): Full replacement content for the invoice, including `invoice_id`, plus everything `create_invoice` accepts. Set only when `resource_type` is `"invoice"`. The recipient (`primary_recipients`) can only be changed 2 times within any 72-hour window. +- `recurring_series_update` (object, optional): Full replacement content for the series, including `recurring_series_id`, plus everything `create_recurring_series` accepts. Set only when `resource_type` is `"recurring_series"`. + +> **Example Prompt**: +> Update invoice {invoice_id} to change the due date to {due_date} +> Update recurring series {recurring_series_id} to change the billing amount to {amount} + +--- + +**`cancel_invoice_auto_reminder`** - Permanently cancels every automatic reminder scheduled for a specific invoice. This action is irreversible. + +- `invoice_id` (string, required): The ID of the invoice for which to cancel all scheduled automatic reminders. + +> **Example Prompt**: +> Cancel all auto reminders for invoice {invoice_id} + +--- + +**`record_payment_for_invoice`** - Records an external or manual payment (cash, check, bank transfer, or a PayPal transaction) against an invoice. Does not process a new payment -- only logs one that was already collected. + +- `invoice_id` (string, required): The ID of the invoice to record the payment against. +- `method` (string, required): The payment method. One of `BANK_TRANSFER`, `CASH`, `CHECK`, `CREDIT_CARD`, `DEBIT_CARD`, `PAYPAL`, `WIRE_TRANSFER`, `OTHER`. +- `payment_id` (string, optional): The ID of a PayPal payment transaction. Required for the `PAYPAL` payment type. +- `payment_date` (string, optional): The date the payment was recorded, in `yyyy-MM-dd` format. +- `amount` (object, optional): The currency and amount for the payment. +- `note` (string, optional): A note associated with an external cash or check payment. + +> **Example Prompt**: +> Record a cash payment of {amount} {currency} against invoice {invoice_id} + +--- + +**`record_refund_for_invoice`** - Records a refund against an invoice. Does not process a new refund -- only logs one that was already issued. + +- `invoice_id` (string, required): The ID of the invoice to mark as refunded. +- `method` (string, required): The refund method. One of `BANK_TRANSFER`, `CASH`, `CHECK`, `CREDIT_CARD`, `DEBIT_CARD`, `PAYPAL`, `WIRE_TRANSFER`, `OTHER`. +- `refund_date` (string, optional): The date the refund was recorded, in `yyyy-MM-dd` format. +- `amount` (object, optional): The currency and amount for the refund. + +> **Example Prompt**: +> Record a refund of {amount} {currency} for invoice {invoice_id} + +--- + +**`create_conditional_rules_for_invoice`** - Creates conditional rules on an invoice, such as an early payment discount or an automatic cancellation date. + +- `invoice_id` (string, required): The ID of the invoice for which the conditional rules are to be created. +- `rules` (array, required): The list of conditional rules to create. + - `conditional_rule_type` (string, required): `EARLY_PAYMENT_DISCOUNT` or `AUTO_CANCEL`. + - `conditional_rule_value_type` (string, required only for `EARLY_PAYMENT_DISCOUNT`): `PERCENT` or `AMOUNT`. + - `conditional_rule_value` (string, required only for `EARLY_PAYMENT_DISCOUNT`): The discount value; must be between 1 and 100 when the value type is `PERCENT`. + - `rule_expiry_terms` (object, required): When the rule expires -- a specific date (`condition_rule_end_date`) or a period relative to the invoice issue date. + +> **Example Prompt**: +> Add an early payment discount of {percent}% to invoice {invoice_id} if paid within 7 days of issue + ### **Payments** --- diff --git a/README.md b/README.md index 51cc5b7a..ffa8e014 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,14 @@ The PayPal Agent toolkit provides the following tools: - `send_invoice_reminder`: Send a reminder for an existing invoice - `cancel_sent_invoice`: Cancel a sent invoice - `generate_invoice_qr_code`: Generate a QR code for an invoice +- `delete_invoice`: Permanently delete a draft or scheduled invoice +- `generate_invoice_number`: Generate the next invoice number available to the merchant +- `search_invoicing`: Search for invoices or recurring invoice series +- `update_invoicing`: Update an existing invoice or recurring invoice series (full-replacement) +- `cancel_invoice_auto_reminder`: Cancel all scheduled automatic reminders for an invoice +- `record_payment_for_invoice`: Record an external or manual payment against an invoice +- `record_refund_for_invoice`: Record a refund against an invoice +- `create_conditional_rules_for_invoice`: Create conditional rules for an invoice, such as an early payment discount or automatic cancellation date **Payments** @@ -96,7 +104,15 @@ const paypalToolkit = new PayPalAgentToolkit({ send: true, sendReminder: true, cancel: true, + delete: true, generateQRC: true, + generateInvoiceNumber: true, + search: true, + update: true, + cancelReminders: true, + recordPayment: true, + recordRefund: true, + createConditionalRules: true, }, products: { create: true, list: true, update: true }, subscriptionPlans: { create: true, list: true, show: true }, diff --git a/python/README.md b/python/README.md index 3eb556c1..adacac28 100644 --- a/python/README.md +++ b/python/README.md @@ -15,6 +15,14 @@ The PayPal Agent toolkit provides the following tools: - `send_invoice_reminder`: Send a reminder for an existing invoice - `cancel_sent_invoice`: Cancel a sent invoice - `generate_invoice_qr_code`: Generate a QR code for an invoice +- `delete_invoice`: Permanently delete a draft or scheduled invoice +- `generate_invoice_number`: Generate the next invoice number available to the merchant +- `search_invoicing`: Search for invoices or recurring invoice series +- `update_invoicing`: Update an existing invoice or recurring invoice series (full-replacement) +- `cancel_invoice_auto_reminder`: Cancel all scheduled automatic reminders for an invoice +- `record_payment_for_invoice`: Record an external or manual payment against an invoice +- `record_refund_for_invoice`: Record a refund against an invoice +- `create_conditional_rules_for_invoice`: Create conditional rules for an invoice, such as an early payment discount or automatic cancellation date **Payments** diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 688cfbaf..89d88b8a 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -542,3 +542,35 @@ def _empty_string_to_none(cls, v): return None if v == "" else v +class InvoiceConditionalRuleExpiryTerms(BaseModel): + rule_expiry_condition: Literal[ + "SPECIFIC_DATE", + "THREE_DAYS_AFTER_ISSUE_DATE", + "SEVEN_DAYS_AFTER_ISSUE_DATE", + "FIFTEEN_DAYS_AFTER_ISSUE_DATE", + "THIRTY_DAYS_AFTER_ISSUE_DATE", + ] = Field(..., description="When the conditional rule expires: a specific date, or a period relative to the invoice issue date.") + condition_rule_end_date: str = Field(..., pattern=DATE_NO_TIME_REGEX.pattern, description="The date the conditional rule expires, in yyyy-MM-dd format.") + + +class InvoiceConditionalRule(BaseModel): + conditional_rule_type: Literal["EARLY_PAYMENT_DISCOUNT", "AUTO_CANCEL"] = Field(..., description="The type of conditional rule to apply to the invoice.") + conditional_rule_value_type: Optional[Literal["PERCENT", "AMOUNT"]] = Field(None, description="The type of the conditional rule value. Required, and only applicable, when conditional_rule_type is EARLY_PAYMENT_DISCOUNT.") + conditional_rule_value: Optional[str] = Field(None, description="The value of the conditional rule. Required, and only applicable, when conditional_rule_type is EARLY_PAYMENT_DISCOUNT. When conditional_rule_value_type is PERCENT, must be between 1 and 100.") + rule_expiry_terms: InvoiceConditionalRuleExpiryTerms = Field(..., description="The expiry terms for the conditional rule.") + + @model_validator(mode="after") + def _validate_early_payment_discount_fields(self): + if self.conditional_rule_type == "EARLY_PAYMENT_DISCOUNT": + if self.conditional_rule_value_type is None or self.conditional_rule_value is None: + raise ValueError("conditional_rule_value_type and conditional_rule_value are required when conditional_rule_type is EARLY_PAYMENT_DISCOUNT.") + if self.conditional_rule_value_type == "PERCENT" and not (1 <= float(self.conditional_rule_value) <= 100): + raise ValueError("conditional_rule_value must be between 1 and 100 when conditional_rule_value_type is PERCENT.") + return self + + +class CreateConditionalRulesForInvoiceParameters(BaseModel): + invoice_id: str = Field(..., pattern=INVOICE_ID_REGEX.pattern, description="The ID of the invoice for which the conditional rules are to be created.") + rules: List[InvoiceConditionalRule] = Field(..., min_length=1, description="The list of conditional rules to create for the invoice.") + + diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index a268b677..2a1d17c1 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -116,4 +116,8 @@ This function records a refund against an invoice, by invoice ID. If all payments on the invoice are refunded, PayPal marks the invoice REFUNDED; otherwise it is marked PARTIALLY_REFUNDED. This does not process a new refund -- it only logs one that was already issued. method is required. +""" + +CREATE_CONDITIONAL_RULES_FOR_INVOICE_PROMPT = """ +Create conditional rules for an invoice on PayPal. """ \ No newline at end of file diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index afe4ccbe..6964212e 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -304,3 +304,18 @@ def record_refund_for_invoice(client, params: dict): return json.dumps({"success": True, "invoice_id": invoice_id}) return json.dumps(response) + + +def create_conditional_rules_for_invoice(client, params: dict): + + validated = CreateConditionalRulesForInvoiceParameters(**params) + invoice_id = validated.invoice_id + payload = validated.model_dump(exclude_none=True, exclude={"invoice_id"}) + + url = f"/v2/invoicing/invoices/{invoice_id}/conditional-rules" + response = client.post(uri=url, payload=payload) + + if not response: + return json.dumps({"success": True, "invoice_id": invoice_id}) + + return json.dumps(response) diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index f69696f4..dc88a897 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -34,6 +34,7 @@ CANCEL_INVOICE_AUTO_REMINDER_PROMPT, RECORD_PAYMENT_FOR_INVOICE_PROMPT, RECORD_REFUND_FOR_INVOICE_PROMPT, + CREATE_CONDITIONAL_RULES_FOR_INVOICE_PROMPT, ) from ..shared.disputes.prompts import ( @@ -94,6 +95,7 @@ CancelInvoiceAutoReminderParameters, RecordPaymentForInvoiceParameters, RecordRefundForInvoiceParameters, + CreateConditionalRulesForInvoiceParameters, ) from ..shared.disputes.parameters import ( @@ -153,6 +155,7 @@ cancel_invoice_auto_reminder, record_payment_for_invoice, record_refund_for_invoice, + create_conditional_rules_for_invoice, ) @@ -419,6 +422,14 @@ "actions": {"invoices": {"recordRefund": True}}, "execute": record_refund_for_invoice, }, + { + "method": "create_conditional_rules_for_invoice", + "name": "Create Conditional Rules For Invoice", + "description": CREATE_CONDITIONAL_RULES_FOR_INVOICE_PROMPT.strip(), + "args_schema": CreateConditionalRulesForInvoiceParameters, + "actions": {"invoices": {"createConditionalRules": True}}, + "execute": create_conditional_rules_for_invoice, + }, { "method": "list_disputes", "name": "List Disputes", diff --git a/typescript/README.md b/typescript/README.md index 253565d7..480138d4 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -16,6 +16,14 @@ The PayPal Agent toolkit provides the following tools: - `send_invoice_reminder`: Send a reminder for an existing invoice - `cancel_sent_invoice`: Cancel a sent invoice - `generate_invoice_qr_code`: Generate a QR code for an invoice +- `delete_invoice`: Permanently delete a draft or scheduled invoice +- `generate_invoice_number`: Generate the next invoice number available to the merchant +- `search_invoicing`: Search for invoices or recurring invoice series +- `update_invoicing`: Update an existing invoice or recurring invoice series (full-replacement) +- `cancel_invoice_auto_reminder`: Cancel all scheduled automatic reminders for an invoice +- `record_payment_for_invoice`: Record an external or manual payment against an invoice +- `record_refund_for_invoice`: Record a refund against an invoice +- `create_conditional_rules_for_invoice`: Create conditional rules for an invoice, such as an early payment discount or automatic cancellation date **Payments** @@ -91,7 +99,15 @@ const paypalToolkit = new PayPalAgentToolkit({ send: true, sendReminder: true, cancel: true, + delete: true, generateQRC: true, + generateInvoiceNumber: true, + search: true, + update: true, + cancelReminders: true, + recordPayment: true, + recordRefund: true, + createConditionalRules: true, }, products: { create: true, list: true, update: true }, subscriptionPlans: { create: true, list: true, show: true }, diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index b074e245..3f38270c 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -24,6 +24,7 @@ import { generateInvoiceNumber, recordPaymentForInvoice, recordRefundForInvoice, + createConditionalRulesForInvoice, createOrder, getOrder, listDisputes, @@ -132,6 +133,8 @@ class PayPalAPI { return recordPaymentForInvoice(this.paypalClient, this.context, arg); case 'record_refund_for_invoice': return recordRefundForInvoice(this.paypalClient, this.context, arg); + case 'create_conditional_rules_for_invoice': + return createConditionalRulesForInvoice(this.paypalClient, this.context, arg); case 'create_product': return createProduct(this.paypalClient, this.context, arg); case 'list_products': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index c49162c6..01b29c16 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -40,6 +40,7 @@ import { updateInvoiceAutoReminderParameters, recordPaymentForInvoiceParameters, recordRefundForInvoiceParameters, + createConditionalRulesForInvoiceParameters, searchInvoicingParameters, updateInvoicingParameters, updateInvoiceBodyParameters, @@ -721,6 +722,36 @@ export async function recordRefundForInvoice( } } +export async function createConditionalRulesForInvoice( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[createConditionalRulesForInvoice] Starting to create conditional rules for invoice'); + const { invoice_id, rules } = params; + + const body = { rules }; + + const headers = await client.getHeaders(); + logger('[createConditionalRulesForInvoice] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/invoices/${invoice_id}/conditional-rules`; + + try { + logger('[createConditionalRulesForInvoice] Sending request to PayPal API'); + const response = await axios.post(url, body, { headers }); + if (response.status === 204) { + logger(`[createConditionalRulesForInvoice] Conditional rules created successfully. Status: ${response.status}`); + return { success: true, invoice_id }; + } + logger(`[createConditionalRulesForInvoice] Response received. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[createConditionalRulesForInvoice] Error creating conditional rules for invoice:', error.message); + handleAxiosError(error); + } +} + // === PRODUCT FUNCTIONS === export async function createProduct( client: PayPalClient, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index d860fdd0..e76f9f07 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -353,6 +353,34 @@ export const recordRefundForInvoiceParameters = (context: Context) => method: z.enum(['BANK_TRANSFER', 'CASH', 'CHECK', 'CREDIT_CARD', 'DEBIT_CARD', 'PAYPAL', 'WIRE_TRANSFER', 'OTHER']).describe('The payment mode or method through which the invoicer can accept the payments.'), }).describe('Record a refund against an invoice.'); +const invoiceConditionalRule = z.object({ + conditional_rule_type: z.enum(['EARLY_PAYMENT_DISCOUNT', 'AUTO_CANCEL']).describe('The type of conditional rule to apply to the invoice.'), + conditional_rule_value_type: z.enum(['PERCENT', 'AMOUNT']).optional().describe('The type of the conditional rule value. Required, and only applicable, when conditional_rule_type is EARLY_PAYMENT_DISCOUNT.'), + conditional_rule_value: z.string().optional().describe('The value of the conditional rule. Required, and only applicable, when conditional_rule_type is EARLY_PAYMENT_DISCOUNT. When conditional_rule_value_type is PERCENT, must be between 1 and 100.'), + rule_expiry_terms: z.object({ + rule_expiry_condition: z.enum([ + 'SPECIFIC_DATE', + 'THREE_DAYS_AFTER_ISSUE_DATE', + 'SEVEN_DAYS_AFTER_ISSUE_DATE', + 'FIFTEEN_DAYS_AFTER_ISSUE_DATE', + 'THIRTY_DAYS_AFTER_ISSUE_DATE', + ]).describe('When the conditional rule expires: a specific date, or a period relative to the invoice issue date.'), + condition_rule_end_date: z.string().regex(DATE_NO_TIME_REGEX, "condition_rule_end_date must be in yyyy-MM-DD format").describe('The date the conditional rule expires, in yyyy-MM-dd format.'), + }).describe('The expiry terms for the conditional rule.'), +}).refine( + (rule) => rule.conditional_rule_type !== 'EARLY_PAYMENT_DISCOUNT' || (rule.conditional_rule_value_type !== undefined && rule.conditional_rule_value !== undefined), + { message: 'conditional_rule_value_type and conditional_rule_value are required when conditional_rule_type is EARLY_PAYMENT_DISCOUNT.' } +).refine( + (rule) => rule.conditional_rule_value_type !== 'PERCENT' || (Number(rule.conditional_rule_value) >= 1 && Number(rule.conditional_rule_value) <= 100), + { message: 'conditional_rule_value must be between 1 and 100 when conditional_rule_value_type is PERCENT.' } +); + +export const createConditionalRulesForInvoiceParameters = (context: Context) => + z.object({ + invoice_id: z.string().regex(INVOICE_ID_REGEX, "Invalid PayPal Invoice ID").describe('The ID of the invoice for which the conditional rules are to be created.'), + rules: z.array(invoiceConditionalRule).min(1).describe('The list of conditional rules to create for the invoice.'), + }).describe('Create conditional rules for an invoice.'); + // search_invoicing: one external tool that internally branches to invoice search or // recurring-series search based on resource_type. invoice_filters/recurring_series_filters diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index 979bb332..cdc1f6b7 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -120,6 +120,10 @@ This function records a refund against an invoice, by invoice ID. If all payment method is required. `; +export const createConditionalRulesForInvoicePrompt = (context: Context) => ` +Create conditional rules for an invoice on PayPal. +`; + export const createProductPrompt = (context: Context) => ` Create a product in PayPal using product catalog - create products API. This function creates a new product that will be used in subscription plans, subscriptions. diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index c2bfe89b..28385ce6 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -21,6 +21,7 @@ import { generateInvoiceNumberPrompt, recordPaymentForInvoicePrompt, recordRefundForInvoicePrompt, + createConditionalRulesForInvoicePrompt, createOrderPrompt, getOrderPrompt, updateShipmentTrackingPrompt, @@ -67,6 +68,7 @@ import { generateInvoiceNumberParameters, recordPaymentForInvoiceParameters, recordRefundForInvoiceParameters, + createConditionalRulesForInvoiceParameters, createOrderParameters, getOrderParameters, updateShipmentTrackingParameters, @@ -305,6 +307,17 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'create_conditional_rules_for_invoice', + name: 'Create Conditional Rules For Invoice', + description: createConditionalRulesForInvoicePrompt(context), + parameters: createConditionalRulesForInvoiceParameters(context), + actions: { + invoices: { + createConditionalRules: true, + }, + }, + }, { method: 'create_product', name: 'Create Product', From 6c64ff7f52477b645eaa32bd26609f368179fb8c Mon Sep 17 00:00:00 2001 From: asbhaskar_paypal Date: Thu, 6 Aug 2026 10:15:04 +0530 Subject: [PATCH 9/9] added more recurring invoice tools (cherry picked from commit 54332be122ee29f0ca2b02da2d188a713f1a75dd) --- README.md | 7 +- python/README.md | 6 ++ .../shared/invoices/parameters.py | 12 +++ .../shared/invoices/prompts.py | 18 +++++ .../shared/invoices/tool_handlers.py | 33 ++++++++ .../shared/paypal_client.py | 2 +- python/paypal_agent_toolkit/shared/tools.py | 33 ++++++++ typescript/README.md | 6 ++ typescript/src/shared/api.ts | 9 +++ typescript/src/shared/functions.ts | 75 +++++++++++++++++++ typescript/src/shared/parameters.ts | 18 +++++ typescript/src/shared/prompts.ts | 18 +++++ typescript/src/shared/tools.ts | 39 ++++++++++ 13 files changed, 274 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ffa8e014..6dc6944d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ The PayPal Agent toolkit provides the following tools: **Invoices** - `create_invoice`: Create a new invoice in the PayPal system, including recipient billing details, line items, an invoice note, a custom color theme, an optional shipping cost, and an option to enable PAY_BY_BANK as a payment method -- `create_recurring_series`: Create a recurring invoice series that automatically generates and sends invoices on a schedule - `list_invoices`: List invoices with optional pagination and filtering - `get_invoice`: Retrieve details of a specific invoice - `send_invoice`: Send an invoice to recipients @@ -26,6 +25,12 @@ The PayPal Agent toolkit provides the following tools: - `record_refund_for_invoice`: Record a refund against an invoice - `create_conditional_rules_for_invoice`: Create conditional rules for an invoice, such as an early payment discount or automatic cancellation date +- `create_recurring_series`: Create a recurring invoice series that automatically generates and sends invoices on a schedule +- `activate_recurring_series`: Activate a draft recurring invoice series +- `get_recurring_series`: Retrieve details of a specific recurring invoice series +- `cancel_recurring_series`: Cancel an active recurring invoice series +- `delete_recurring_series`: Delete a draft recurring invoice series + **Payments** - `create_order`: Create an order in PayPal system based on provided details diff --git a/python/README.md b/python/README.md index adacac28..5fbc75ee 100644 --- a/python/README.md +++ b/python/README.md @@ -24,6 +24,12 @@ The PayPal Agent toolkit provides the following tools: - `record_refund_for_invoice`: Record a refund against an invoice - `create_conditional_rules_for_invoice`: Create conditional rules for an invoice, such as an early payment discount or automatic cancellation date +- `create_recurring_series`: Create a recurring invoice series that automatically generates and sends invoices on a schedule +- `activate_recurring_series`: Activate a draft recurring invoice series +- `get_recurring_series`: Retrieve details of a specific recurring invoice series +- `cancel_recurring_series`: Cancel an active recurring invoice series +- `delete_recurring_series`: Delete a draft recurring invoice series + **Payments** - `create_order`: Create an order in PayPal system based on provided details diff --git a/python/paypal_agent_toolkit/shared/invoices/parameters.py b/python/paypal_agent_toolkit/shared/invoices/parameters.py index 89d88b8a..24f896a9 100644 --- a/python/paypal_agent_toolkit/shared/invoices/parameters.py +++ b/python/paypal_agent_toolkit/shared/invoices/parameters.py @@ -190,6 +190,18 @@ class ActivateRecurringSeriesParameters(BaseModel): recurring_series_id: str = Field(..., description="The ID of the recurring invoice series to activate.", pattern=RECURRING_SERIES_ID_REGEX.pattern) +class GetRecurringSeriesParameters(BaseModel): + recurring_series_id: str = Field(..., description="The ID of the recurring invoice series to retrieve.", pattern=RECURRING_SERIES_ID_REGEX.pattern) + + +class CancelRecurringSeriesParameters(BaseModel): + recurring_series_id: str = Field(..., description="The ID of the recurring invoice series to cancel.", pattern=RECURRING_SERIES_ID_REGEX.pattern) + + +class DeleteRecurringSeriesParameters(BaseModel): + recurring_series_id: str = Field(..., description="The ID of the recurring invoice series to delete. Only series in DRAFT status can be deleted; use CancelRecurringSeriesParameters for an activated series.", pattern=RECURRING_SERIES_ID_REGEX.pattern) + + class GetInvoiceParameters(BaseModel): invoice_id: str = Field(..., description="The ID of the invoice to retrieve.", pattern=INVOICE_ID_REGEX) diff --git a/python/paypal_agent_toolkit/shared/invoices/prompts.py b/python/paypal_agent_toolkit/shared/invoices/prompts.py index 2a1d17c1..a711e874 100644 --- a/python/paypal_agent_toolkit/shared/invoices/prompts.py +++ b/python/paypal_agent_toolkit/shared/invoices/prompts.py @@ -22,6 +22,24 @@ This function activates a recurring invoice series by its ID, moving it out of DRAFT status. Once activated, PayPal automatically generates and sends invoices to the customer based on the series' configured schedule. Call this after create_recurring_series to make the series active. """ +GET_RECURRING_SERIES_PROMPT = """ +Get a recurring invoice series from PayPal. + +This function retrieves details of a specific recurring invoice series using its ID, including its schedule, status, template, and recipient information. +""" + +CANCEL_RECURRING_SERIES_PROMPT = """ +Cancel a recurring invoice series on PayPal. + +This function cancels a recurring invoice series by its ID. Once cancelled, PayPal stops generating and sending further invoices for the series. This action cannot be undone. +""" + +DELETE_RECURRING_SERIES_PROMPT = """ +Delete a recurring invoice series on PayPal. + +This function permanently deletes a recurring invoice series by its ID. Only series in DRAFT status can be deleted -- for a series that has already been activated, use cancel_recurring_series instead. This action cannot be undone. +""" + LIST_INVOICE_PROMPT = """ List invoices from PayPal. diff --git a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py index 6964212e..b1110f96 100644 --- a/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py +++ b/python/paypal_agent_toolkit/shared/invoices/tool_handlers.py @@ -75,6 +75,39 @@ def activate_recurring_series(client, params: dict): return json.dumps({"recurring_series_id": recurring_series_id, "status": "ACTIVATED"}) +def get_recurring_series(client, params: dict): + + validated = GetRecurringSeriesParameters(**params) + recurring_series_id = validated.recurring_series_id + + url = f"/v2/invoicing/recurring-invoices/{recurring_series_id}" + response = client.get(uri=url) + + return json.dumps(response) + + +def cancel_recurring_series(client, params: dict): + + validated = CancelRecurringSeriesParameters(**params) + recurring_series_id = validated.recurring_series_id + + url = f"/v2/invoicing/recurring-invoices/{recurring_series_id}/cancel" + client.post(uri=url, payload={}) + + return json.dumps({"recurring_series_id": recurring_series_id, "status": "CANCELLED"}) + + +def delete_recurring_series(client, params: dict): + + validated = DeleteRecurringSeriesParameters(**params) + recurring_series_id = validated.recurring_series_id + + url = f"/v2/invoicing/recurring-invoices/{recurring_series_id}" + client.delete(uri=url) + + return json.dumps({"recurring_series_id": recurring_series_id, "status": "DELETED"}) + + def list_invoices(client, params: dict): validated = ListInvoicesParameters(**params) diff --git a/python/paypal_agent_toolkit/shared/paypal_client.py b/python/paypal_agent_toolkit/shared/paypal_client.py index 08f6c276..cd4c554f 100644 --- a/python/paypal_agent_toolkit/shared/paypal_client.py +++ b/python/paypal_agent_toolkit/shared/paypal_client.py @@ -166,7 +166,7 @@ def delete(self, uri): try: json_response = response.json() except ValueError: - logging.warning("Response body is not valid JSON or empty") + logging.warning("Response body is not valid JSON or empty, Headers: %s", json.dumps(dict(response.headers), indent=2)) return {} logResponsePayload(response, json_response) diff --git a/python/paypal_agent_toolkit/shared/tools.py b/python/paypal_agent_toolkit/shared/tools.py index dc88a897..4957511e 100644 --- a/python/paypal_agent_toolkit/shared/tools.py +++ b/python/paypal_agent_toolkit/shared/tools.py @@ -19,6 +19,9 @@ CREATE_INVOICE_PROMPT, CREATE_RECURRING_SERIES_PROMPT, ACTIVATE_RECURRING_SERIES_PROMPT, + GET_RECURRING_SERIES_PROMPT, + CANCEL_RECURRING_SERIES_PROMPT, + DELETE_RECURRING_SERIES_PROMPT, LIST_INVOICE_PROMPT, GET_INVOICE_PROMPT, SEND_INVOICE_PROMPT, @@ -80,6 +83,9 @@ CreateInvoiceParameters, CreateRecurringSeriesParameters, ActivateRecurringSeriesParameters, + GetRecurringSeriesParameters, + CancelRecurringSeriesParameters, + DeleteRecurringSeriesParameters, SendInvoiceParameters, ListInvoicesParameters, GetInvoiceParameters, @@ -140,6 +146,9 @@ create_invoice, create_recurring_series, activate_recurring_series, + get_recurring_series, + cancel_recurring_series, + delete_recurring_series, send_invoice, list_invoices, get_invoice, @@ -302,6 +311,30 @@ "actions": {"invoices": {"activateRecurringSeries": True}}, "execute": activate_recurring_series, }, + { + "method": "get_recurring_series", + "name": "Get Recurring Invoice Series", + "description": GET_RECURRING_SERIES_PROMPT.strip(), + "args_schema": GetRecurringSeriesParameters, + "actions": {"invoices": {"getRecurringSeries": True}}, + "execute": get_recurring_series, + }, + { + "method": "cancel_recurring_series", + "name": "Cancel Recurring Invoice Series", + "description": CANCEL_RECURRING_SERIES_PROMPT.strip(), + "args_schema": CancelRecurringSeriesParameters, + "actions": {"invoices": {"cancelRecurringSeries": True}}, + "execute": cancel_recurring_series, + }, + { + "method": "delete_recurring_series", + "name": "Delete Recurring Invoice Series", + "description": DELETE_RECURRING_SERIES_PROMPT.strip(), + "args_schema": DeleteRecurringSeriesParameters, + "actions": {"invoices": {"deleteRecurringSeries": True}}, + "execute": delete_recurring_series, + }, { "method": "list_invoices", "name": "List Invoices", diff --git a/typescript/README.md b/typescript/README.md index 480138d4..1b207815 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -25,6 +25,12 @@ The PayPal Agent toolkit provides the following tools: - `record_refund_for_invoice`: Record a refund against an invoice - `create_conditional_rules_for_invoice`: Create conditional rules for an invoice, such as an early payment discount or automatic cancellation date +- `create_recurring_series`: Create a recurring invoice series that automatically generates and sends invoices on a schedule +- `activate_recurring_series`: Activate a draft recurring invoice series +- `get_recurring_series`: Retrieve details of a specific recurring invoice series +- `cancel_recurring_series`: Cancel an active recurring invoice series +- `delete_recurring_series`: Delete a draft recurring invoice series + **Payments** - `create_order`: Create an order in PayPal system based on provided details diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index 3f38270c..a37fb8a2 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -2,6 +2,9 @@ import { createInvoice, createRecurringSeries, activateRecurringSeries, + getRecurringSeries, + cancelRecurringSeries, + deleteRecurringSeries, listInvoices, getInvoice, sendInvoice, @@ -103,6 +106,12 @@ class PayPalAPI { return createRecurringSeries(this.paypalClient, this.context, arg); case 'activate_recurring_series': return activateRecurringSeries(this.paypalClient, this.context, arg); + case 'get_recurring_series': + return getRecurringSeries(this.paypalClient, this.context, arg); + case 'cancel_recurring_series': + return cancelRecurringSeries(this.paypalClient, this.context, arg); + case 'delete_recurring_series': + return deleteRecurringSeries(this.paypalClient, this.context, arg); case 'list_invoices': return listInvoices(this.paypalClient, this.context, arg); case 'get_invoice': diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index 01b29c16..1d8a248e 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -7,6 +7,9 @@ import { createInvoiceParameters, createRecurringSeriesParameters, activateRecurringSeriesParameters, + getRecurringSeriesParameters, + cancelRecurringSeriesParameters, + deleteRecurringSeriesParameters, createOrderParameters, generateInvoiceQrCodeParameters, generateInvoiceNumberParameters, @@ -201,6 +204,30 @@ async function updateInvoice( } } +export async function getRecurringSeries( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[getRecurringSeries] Starting to get recurring series'); + + const headers = await client.getHeaders(); + logger('[getRecurringSeries] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/recurring-invoices/${params.recurring_series_id}`; + logger(`[getRecurringSeries] API URL: ${url}`); + + try { + logger('[getRecurringSeries] Sending request to PayPal API'); + const response = await axios.get(url, { headers }); + logger(`[getRecurringSeries] Recurring series retrieved successfully. Status: ${response.status}`); + return response.data; + } catch (error: any) { + logger('[getRecurringSeries] Error getting recurring series:', error.message); + handleAxiosError(error); + } +} + async function updateRecurringSeries( client: PayPalClient, context: Context, @@ -225,6 +252,30 @@ async function updateRecurringSeries( } } +export async function cancelRecurringSeries( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[cancelRecurringSeries] Starting recurring series cancellation process'); + + const headers = await client.getHeaders(); + logger('[cancelRecurringSeries] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/recurring-invoices/${params.recurring_series_id}/cancel`; + logger(`[cancelRecurringSeries] API URL: ${url}`); + + try { + logger('[cancelRecurringSeries] Sending request to PayPal API'); + const response = await axios.post(url, {}, { headers }); + logger(`[cancelRecurringSeries] Recurring series cancelled successfully. Status: ${response.status}`); + return { recurring_series_id: params.recurring_series_id, status: response.status }; + } catch (error: any) { + logger('[cancelRecurringSeries] Error cancelling recurring series:', error.message); + handleAxiosError(error); + } +} + export async function updateInvoicing( client: PayPalClient, context: Context, @@ -254,6 +305,30 @@ export async function updateInvoicing( return updateRecurringSeries(client, context, params.recurring_series_update as TypeOf> & { recurring_series_id: string }); } +export async function deleteRecurringSeries( + client: PayPalClient, + context: Context, + params: TypeOf> +) { + logger('[deleteRecurringSeries] Starting recurring series deletion process'); + + const headers = await client.getHeaders(); + logger('[deleteRecurringSeries] Headers obtained'); + + const url = `${client.getBaseUrl()}/v2/invoicing/recurring-invoices/${params.recurring_series_id}`; + logger(`[deleteRecurringSeries] API URL: ${url}`); + + try { + logger('[deleteRecurringSeries] Sending request to PayPal API'); + const response = await axios.delete(url, { headers }); + logger(`[deleteRecurringSeries] Recurring series deleted successfully. Status: ${response.status}`); + return { recurring_series_id: params.recurring_series_id, status: response.status }; + } catch (error: any) { + logger('[deleteRecurringSeries] Error deleting recurring series:', error.message); + handleAxiosError(error); + } +} + export async function listInvoices( client: PayPalClient, context: Context, diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index e76f9f07..a0499ace 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -243,6 +243,24 @@ export const activateRecurringSeriesParameters = (context: Context) => z.object( .describe("The ID of the recurring invoice series to activate."), }); +export const getRecurringSeriesParameters = (context: Context) => z.object({ + recurring_series_id: z.string() + .regex(RECURRING_SERIES_ID_REGEX, "Invalid PayPal Recurring Series ID") + .describe("The ID of the recurring invoice series to retrieve."), +}); + +export const cancelRecurringSeriesParameters = (context: Context) => z.object({ + recurring_series_id: z.string() + .regex(RECURRING_SERIES_ID_REGEX, "Invalid PayPal Recurring Series ID") + .describe("The ID of the recurring invoice series to cancel."), +}); + +export const deleteRecurringSeriesParameters = (context: Context) => z.object({ + recurring_series_id: z.string() + .regex(RECURRING_SERIES_ID_REGEX, "Invalid PayPal Recurring Series ID") + .describe("The ID of the recurring invoice series to delete. Only series in DRAFT status can be deleted; use cancel_recurring_series for an activated series."), +}); + export const getInvoicParameters = (context: Context) => z.object({ invoice_id: z.string() .regex(INVOICE_ID_REGEX, "Invalid PayPal Invoice ID") diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index cdc1f6b7..64ea2893 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -32,6 +32,24 @@ Activate a recurring invoice series on PayPal. This function activates a recurring invoice series by its ID, moving it out of DRAFT status. Once activated, PayPal automatically generates and sends invoices to the customer based on the series' configured schedule. Call this after create_recurring_series to make the series active. `; +export const getRecurringSeriesPrompt = (context: Context) => ` +Get a recurring invoice series from PayPal. + +This function retrieves details of a specific recurring invoice series using its ID, including its schedule, status, template, and recipient information. +`; + +export const cancelRecurringSeriesPrompt = (context: Context) => ` +Cancel a recurring invoice series on PayPal. + +This function cancels a recurring invoice series by its ID. Once cancelled, PayPal stops generating and sending further invoices for the series. This action cannot be undone. +`; + +export const deleteRecurringSeriesPrompt = (context: Context) => ` +Delete a recurring invoice series on PayPal. + +This function permanently deletes a recurring invoice series by its ID. Only series in DRAFT status can be deleted -- for a series that has already been activated, use cancel_recurring_series instead. This action cannot be undone. +`; + export const listInvoicesPrompt = (context: Context) => ` List invoices from PayPal. diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index 28385ce6..1b561b30 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -4,6 +4,9 @@ import { createInvoicePrompt, createRecurringSeriesPrompt, activateRecurringSeriesPrompt, + getRecurringSeriesPrompt, + cancelRecurringSeriesPrompt, + deleteRecurringSeriesPrompt, listInvoicesPrompt, getInvoicePrompt, sendInvoicePrompt, @@ -51,6 +54,9 @@ import { createInvoiceParameters, createRecurringSeriesParameters, activateRecurringSeriesParameters, + getRecurringSeriesParameters, + cancelRecurringSeriesParameters, + deleteRecurringSeriesParameters, listInvoicesParameters, getInvoicParameters, sendInvoiceParameters, @@ -142,6 +148,39 @@ const tools = (context: Context): Tool[] => [ }, }, }, + { + method: 'get_recurring_series', + name: 'Get Recurring Invoice Series', + description: getRecurringSeriesPrompt(context), + parameters: getRecurringSeriesParameters(context), + actions: { + invoices: { + getRecurringSeries: true, + }, + }, + }, + { + method: 'cancel_recurring_series', + name: 'Cancel Recurring Invoice Series', + description: cancelRecurringSeriesPrompt(context), + parameters: cancelRecurringSeriesParameters(context), + actions: { + invoices: { + cancelRecurringSeries: true, + }, + }, + }, + { + method: 'delete_recurring_series', + name: 'Delete Recurring Invoice Series', + description: deleteRecurringSeriesPrompt(context), + parameters: deleteRecurringSeriesParameters(context), + actions: { + invoices: { + deleteRecurringSeries: true, + }, + }, + }, { method: 'list_invoices', name: 'List Invoices',