Skip to content

Commit a3830c3

Browse files
ashokkumaruasbhaskar_paypal
andauthored
Add tools for creating themed and recurring invoices (#84)
* added tools for create invoice with theme and recurring invoice * refactored create_invoice tool and added create_recurring_invoice tool * update create invoice and recurring invoice prompts * added partial payment --------- Co-authored-by: asbhaskar_paypal <asbhaskar@paypal.com>
1 parent f4274b8 commit a3830c3

14 files changed

Lines changed: 760 additions & 77 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ The PayPal Agent toolkit provides the following tools:
99

1010
**Invoices**
1111

12-
- `create_invoice`: Create a new invoice in the PayPal system
12+
- `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
13+
- `create_recurring_series`: Create a recurring invoice series that automatically generates and sends invoices on a schedule
1314
- `list_invoices`: List invoices with optional pagination and filtering
1415
- `get_invoice`: Retrieve details of a specific invoice
1516
- `send_invoice`: Send an invoice to recipients
@@ -90,6 +91,7 @@ const paypalToolkit = new PayPalAgentToolkit({
9091
actions: {
9192
invoices: {
9293
create: true,
94+
createRecurringSeries: true,
9395
list: true,
9496
send: true,
9597
sendReminder: true,

python/paypal_agent_toolkit/shared/invoices/parameters.py

Lines changed: 138 additions & 32 deletions
Large diffs are not rendered by default.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
def _compact(obj: dict) -> dict | None:
2+
"""Drops None-valued keys from a shallow dict; returns None if nothing is left,
3+
so callers can omit an entire nested object (e.g. name, address) when none of its
4+
source fields were provided."""
5+
filtered = {k: v for k, v in obj.items() if v is not None}
6+
return filtered or None
7+
8+
9+
def build_create_invoice_payload(params: dict) -> dict:
10+
"""Re-nests the flat CreateInvoiceParameters fields into PayPal's actual
11+
invoicing API request shape."""
12+
currency_code = params["currency_code"]
13+
14+
invoicer = _compact({
15+
"business_name": params.get("invoicer_business_name"),
16+
"name": _compact({
17+
"given_name": params.get("invoicer_given_name"),
18+
"surname": params.get("invoicer_surname"),
19+
}),
20+
"address": _compact({
21+
"address_line_1": params.get("invoicer_address_line_1"),
22+
"address_line_2": params.get("invoicer_address_line_2"),
23+
"admin_area_2": params.get("invoicer_city"),
24+
"admin_area_1": params.get("invoicer_state"),
25+
"postal_code": params.get("invoicer_postal_code"),
26+
"country_code": params.get("invoicer_country_code"),
27+
}),
28+
"email_address": params.get("invoicer_email_address"),
29+
"tax_id": params.get("invoicer_tax_id"),
30+
})
31+
32+
payment_method_overrides = None
33+
if params.get("enable_pay_by_bank"):
34+
rules = None
35+
if params.get("pay_by_bank_exclusive_above_threshold"):
36+
rules = [{
37+
"rule_type": "EXCLUSIVE_ABOVE_AMOUNT_THRESHOLD",
38+
"rule_value": "true",
39+
}]
40+
payment_method_overrides = [_compact({
41+
"payment_method_type": "PAY_BY_BANK",
42+
"enabled": True,
43+
"rules": rules,
44+
})]
45+
46+
partial_payment = None
47+
if params.get("allow_partial_payment") is not None:
48+
partial_payment = _compact({
49+
"allow_partial_payment": params.get("allow_partial_payment"),
50+
"minimum_amount_due": {"currency_code": currency_code, "value": params["minimum_partial_payment_amount"]}
51+
if params.get("minimum_partial_payment_amount") is not None else None,
52+
})
53+
54+
configuration = _compact({
55+
"allow_tip": params.get("allow_tip"),
56+
"theme": {"primary_color": params["theme_color"]} if params.get("theme_color") is not None else None,
57+
"payment_method_overrides": payment_method_overrides,
58+
"partial_payment": partial_payment,
59+
})
60+
61+
amount = None
62+
if params.get("shipping_cost") is not None:
63+
amount = {"breakdown": {"shipping": {"amount": {"currency_code": currency_code, "value": params["shipping_cost"]}}}}
64+
65+
return _compact({
66+
"detail": _compact({
67+
"reference": params.get("reference"),
68+
"invoice_number": params.get("invoice_number"),
69+
"invoice_date": params.get("invoice_date"),
70+
"currency_code": currency_code,
71+
"note": params.get("note"),
72+
}),
73+
"invoicer": invoicer,
74+
"primary_recipients": params.get("primary_recipients", []),
75+
"items": params.get("items", []),
76+
"configuration": configuration,
77+
"amount": amount,
78+
})
79+
80+
81+
def build_create_recurring_series_payload(params: dict) -> dict:
82+
"""Re-nests the flat CreateRecurringSeriesParameters fields into PayPal's actual
83+
recurring-invoicing API request shape."""
84+
currency_code = params["currency_code"]
85+
86+
invoicer = _compact({
87+
"business_name": params.get("invoicer_business_name"),
88+
"name": _compact({
89+
"given_name": params.get("invoicer_given_name"),
90+
"surname": params.get("invoicer_surname"),
91+
}),
92+
"address": _compact({
93+
"address_line_1": params.get("invoicer_address_line_1"),
94+
"address_line_2": params.get("invoicer_address_line_2"),
95+
"admin_area_2": params.get("invoicer_city"),
96+
"admin_area_1": params.get("invoicer_state"),
97+
"postal_code": params.get("invoicer_postal_code"),
98+
"country_code": params.get("invoicer_country_code"),
99+
}),
100+
"email_address": params.get("invoicer_email_address"),
101+
"tax_id": params.get("invoicer_tax_id"),
102+
})
103+
104+
partial_payment = None
105+
if params.get("allow_partial_payment") is not None:
106+
partial_payment = _compact({
107+
"allow_partial_payment": params.get("allow_partial_payment"),
108+
"minimum_amount_due": {"currency_code": currency_code, "value": params["minimum_partial_payment_amount"]}
109+
if params.get("minimum_partial_payment_amount") is not None else None,
110+
})
111+
112+
configuration = _compact({
113+
"allow_tip": params.get("allow_tip"),
114+
"partial_payment": partial_payment,
115+
})
116+
117+
amount = None
118+
if params.get("shipping_cost") is not None:
119+
amount = {"breakdown": {"shipping": {"amount": {"currency_code": currency_code, "value": params["shipping_cost"]}}}}
120+
121+
return {
122+
"plan_detail": _compact({
123+
"frequency": {
124+
"interval_unit": params["interval_unit"],
125+
"interval_count": params["interval_count"],
126+
},
127+
"start_series_date": params.get("start_series_date"),
128+
"total_cycles": params.get("total_cycles"),
129+
}),
130+
"recurring_info": _compact({
131+
"detail": _compact({
132+
"reference": params.get("reference"),
133+
"currency_code": currency_code,
134+
"note": params.get("note"),
135+
}),
136+
"invoicer": invoicer,
137+
"primary_recipients": params.get("primary_recipients", []),
138+
"items": params.get("items"),
139+
"configuration": configuration,
140+
"amount": amount,
141+
}),
142+
}

python/paypal_agent_toolkit/shared/invoices/prompts.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
CREATE_INVOICE_PROMPT = """
22
Create Invoices on PayPal.
33
4-
This function is used to create an invoice in the PayPal system. It allows you to generate a new invoice, specifying details such as customer information, items, quantities, pricing, and tax information. Once created, an invoice can be sent to the customer for payment.
4+
This function creates a draft invoice, specifying a currency code, the invoicer's business information, one or more recipients to bill, and line items. Once created, the invoice can be sent to the customer for payment.
5+
6+
primary_recipients and items use PayPal's real nested shape (billing_info/shipping_info for recipients; unit_amount/tax/discount for items); other fields use a simplified flat shape.
7+
"""
8+
9+
CREATE_RECURRING_SERIES_PROMPT = """
10+
Create a recurring invoice series on PayPal.
11+
12+
This function creates a recurring invoice series that automatically generates and sends invoices to a customer on a schedule, specifying a billing frequency, a start date, a currency code, a primary recipient, and line items for the series template.
13+
14+
primary_recipients and items use PayPal's real nested shape (billing_info/shipping_info for recipients; unit_amount/tax/discount for items); other fields use a simplified flat shape.
15+
16+
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.
17+
"""
18+
19+
ACTIVATE_RECURRING_SERIES_PROMPT = """
20+
Activate a recurring invoice series on PayPal.
21+
22+
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.
523
"""
624

725
LIST_INVOICE_PROMPT = """

python/paypal_agent_toolkit/shared/invoices/tool_handlers.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11

22
from .parameters import *
3+
from .payload_util import build_create_invoice_payload, build_create_recurring_series_payload
34
import json
45
import httpx
56
from typing import Union, Dict, Any
67

78

89

9-
def create_invoice(client, params: dict):
10-
11-
validated = CreateInvoiceParameters(**params)
12-
invoice_payload = validated.model_dump()
13-
10+
def _submit_invoice(client, invoice_payload: dict):
1411
url = "/v2/invoicing/invoices"
1512
response = client.post(uri=url, payload=invoice_payload)
1613

@@ -36,6 +33,14 @@ def create_invoice(client, params: dict):
3633
return json.dumps(response)
3734

3835

36+
def create_invoice(client, params: dict):
37+
38+
validated = CreateInvoiceParameters(**params)
39+
invoice_payload = build_create_invoice_payload(validated.model_dump(exclude_none=True))
40+
41+
return _submit_invoice(client, invoice_payload)
42+
43+
3944
def send_invoice(client, params: dict):
4045

4146
validated = SendInvoiceParameters(**params)
@@ -48,6 +53,28 @@ def send_invoice(client, params: dict):
4853
return json.dumps(response)
4954

5055

56+
def create_recurring_series(client, params: dict):
57+
58+
validated = CreateRecurringSeriesParameters(**params)
59+
payload = build_create_recurring_series_payload(validated.model_dump(exclude_none=True))
60+
61+
url = "/v2/invoicing/recurring-invoices"
62+
response = client.post(uri=url, payload=payload)
63+
64+
return json.dumps(response)
65+
66+
67+
def activate_recurring_series(client, params: dict):
68+
69+
validated = ActivateRecurringSeriesParameters(**params)
70+
recurring_series_id = validated.recurring_series_id
71+
72+
url = f"/v2/invoicing/recurring-invoices/{recurring_series_id}/activate"
73+
client.post(uri=url, payload={})
74+
75+
return json.dumps({"recurring_series_id": recurring_series_id, "status": "ACTIVATED"})
76+
77+
5178
def list_invoices(client, params: dict):
5279

5380
validated = ListInvoicesParameters(**params)

python/paypal_agent_toolkit/shared/regex.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@
1414
DISPUTE_ID_REGEX = re.compile(r"^[A-Za-z0-9_-]{1,255}$")
1515
REFUND_ID_REGEX = re.compile(r"^[A-Za-z0-9_-]{12,32}$")
1616
CAPTURE_ID_REGEX = re.compile(r"^[A-Za-z0-9_-]{12,32}$")
17-
TRANSACTION_ID_REGEX = re.compile(r"^[A-Za-z0-9_-]{12,255}$")
17+
TRANSACTION_ID_REGEX = re.compile(r"^[A-Za-z0-9_-]{12,255}$")
18+
HEX_COLOR_REGEX = re.compile(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
19+
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])$")
20+
RECURRING_SERIES_ID_REGEX = re.compile(r"^RI-[A-Z0-9]{17}$")
21+
COUNTRY_CODE_REGEX = re.compile(r"^([A-Z]{2}|C2)$")
22+
LANGUAGE_REGEX = re.compile(r"^[a-z]{2}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$")
23+
DECIMAL_STRING_REGEX = re.compile(r"^((-?[0-9]+)|(-?([0-9]+)?[.][0-9]+))$")

python/paypal_agent_toolkit/shared/tools.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
from ..shared.invoices.prompts import (
1919
CREATE_INVOICE_PROMPT,
20+
CREATE_RECURRING_SERIES_PROMPT,
21+
ACTIVATE_RECURRING_SERIES_PROMPT,
2022
LIST_INVOICE_PROMPT,
2123
GET_INVOICE_PROMPT,
2224
SEND_INVOICE_PROMPT,
@@ -68,6 +70,8 @@
6870

6971
from ..shared.invoices.parameters import (
7072
CreateInvoiceParameters,
73+
CreateRecurringSeriesParameters,
74+
ActivateRecurringSeriesParameters,
7175
SendInvoiceParameters,
7276
ListInvoicesParameters,
7377
GetInvoiceParameters,
@@ -118,6 +122,8 @@
118122

119123
from ..shared.invoices.tool_handlers import (
120124
create_invoice,
125+
create_recurring_series,
126+
activate_recurring_series,
121127
send_invoice,
122128
list_invoices,
123129
get_invoice,
@@ -256,6 +262,22 @@
256262
"actions": {"invoices": {"create": True}},
257263
"execute": create_invoice,
258264
},
265+
{
266+
"method": "create_recurring_series",
267+
"name": "Create Recurring Invoice Series",
268+
"description": CREATE_RECURRING_SERIES_PROMPT.strip(),
269+
"args_schema": CreateRecurringSeriesParameters,
270+
"actions": {"invoices": {"createRecurringSeries": True}},
271+
"execute": create_recurring_series,
272+
},
273+
{
274+
"method": "activate_recurring_series",
275+
"name": "Activate Recurring Invoice Series",
276+
"description": ACTIVATE_RECURRING_SERIES_PROMPT.strip(),
277+
"args_schema": ActivateRecurringSeriesParameters,
278+
"actions": {"invoices": {"activateRecurringSeries": True}},
279+
"execute": activate_recurring_series,
280+
},
259281
{
260282
"method": "list_invoices",
261283
"name": "List Invoices",

typescript/src/shared/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
22
createInvoice,
3+
createRecurringSeries,
4+
activateRecurringSeries,
35
listInvoices,
46
getInvoice,
57
sendInvoice,
@@ -89,6 +91,10 @@ class PayPalAPI {
8991
switch (method) {
9092
case 'create_invoice':
9193
return createInvoice(this.paypalClient, this.context, arg);
94+
case 'create_recurring_series':
95+
return createRecurringSeries(this.paypalClient, this.context, arg);
96+
case 'activate_recurring_series':
97+
return activateRecurringSeries(this.paypalClient, this.context, arg);
9298
case 'list_invoices':
9399
return listInvoices(this.paypalClient, this.context, arg);
94100
case 'get_invoice':

0 commit comments

Comments
 (0)