-
Notifications
You must be signed in to change notification settings - Fork 103
Feature/paymentlinks #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
vikvenkataraman
wants to merge
11
commits into
paypal:main
Choose a base branch
from
vikvenkataraman:feature/paymentlinks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,446
−5
Draft
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
36071c2
add payment link mcp crud tools
vikvenkataraman 50548dd
add payment link mcp crud tools
vikvenkataraman 779cb0c
minor updates for better prompts
vikvenkataraman 39b661a
fixing typos
vikvenkataraman 243b602
update pr review updates
vikvenkataraman 5e1e5a6
add payment link mcp crud tools
vikvenkataraman 018eaef
add payment link mcp crud tools
vikvenkataraman dd633c2
minor updates for better prompts
vikvenkataraman c1692ac
fixing typos
vikvenkataraman ab3b779
update pr review updates
vikvenkataraman 780133d
resolve conflict
vikvenkataraman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Payment Links module | ||
|
|
||
|
|
95 changes: 95 additions & 0 deletions
95
python/paypal_agent_toolkit/shared/payment_links/parameters.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| from pydantic import BaseModel, Field, field_validator | ||
| from typing import List, Optional, Any, Dict, Literal | ||
|
|
||
|
|
||
| class UnitAmount(BaseModel): | ||
| currency_code: str = Field(..., min_length=3, max_length=3, description="The three-character ISO-4217 currency code (e.g., USD, EUR, GBP, JPY, CAD, AUD). Note: JPY is a zero-decimal currency - use whole numbers without decimal points.") | ||
| value: str = Field(..., max_length=32, pattern=r"^((-?[0-9]+)|(-?([0-9]+)?[.][0-9]+))$", description="The monetary value. Supports up to 3 decimal places for most currencies. For JPY, use whole numbers (e.g., '15000' not '15000.00'). Examples: '29.99', '15000', '9.995'") | ||
|
|
||
|
|
||
| class Tax(BaseModel): | ||
| name: Optional[str] = Field(None, min_length=1, max_length=127, description="Internal label for this tax (e.g., 'Sales Tax', 'VAT', 'GST'). Not visible to customer during checkout.") | ||
| type: Literal['PERCENTAGE', 'PREFERENCE'] = Field(..., description="Tax calculation method. PERCENTAGE: percentage of price (e.g., 8.5% tax). PREFERENCE: uses account default tax settings.") | ||
| value: str = Field(..., min_length=1, max_length=20, description="Tax value. For PERCENTAGE: enter numeric percentage (e.g., '8.5' for 8.5% tax rate). For PREFERENCE: must be 'PROFILE' to use account defaults. Maximum 20 characters.") | ||
|
|
||
|
|
||
| class Shipping(BaseModel): | ||
| type: Literal['FLAT', 'PREFERENCE'] = Field(..., description="Shipping calculation method. FLAT: fixed amount (e.g., $9.99 flat rate). PREFERENCE: uses account default shipping settings.") | ||
| value: str = Field(..., min_length=1, max_length=20, description="Shipping value. For FLAT: enter fixed cost (e.g., '9.99' for $9.99 shipping). For PREFERENCE: must be 'PROFILE' to use account defaults. Maximum 20 characters.") | ||
|
|
||
|
|
||
| class CustomerNote(BaseModel): | ||
| required: Optional[bool] = Field(None, description="When true, customer must provide input before checkout. When false, input is optional. Use for collecting gift messages, custom instructions, engraving text, etc.") | ||
| label: Optional[str] = Field(None, min_length=1, max_length=127, description="The label displayed to customers for this custom input field. Examples: 'Gift message', 'Special instructions', 'Engraving text', 'Delivery notes'") | ||
|
|
||
|
|
||
| class VariantOption(BaseModel): | ||
| """Variant option structure - using passthrough to allow flexible fields""" | ||
| class Config: | ||
| extra = "allow" | ||
|
|
||
|
|
||
| class Dimension(BaseModel): | ||
| name: str = Field(..., min_length=1, max_length=64, description="The name of this variant dimension. Examples: 'Size', 'Color', 'Material', 'Style'. Maximum 64 characters.") | ||
| primary: bool = Field(..., description="IMPORTANT: Exactly ONE dimension must have primary: true, all others must be primary: false. The primary dimension typically contains the main product variation and can include pricing in options. Common pattern: Size=primary, Color=non-primary.") | ||
| options: List[Dict[str, Any]] = Field(..., min_items=1, max_items=10, description="Array of variant options (1-10 options). Each option can have: label (required), unit_amount (optional - only if no product-level unit_amount). Example: [{'label': 'Small'}, {'label': 'Medium'}, {'label': 'Large'}]") | ||
|
|
||
|
|
||
| class Variants(BaseModel): | ||
| dimensions: List[Dimension] = Field(..., min_items=1, max_items=5, description="List of variant dimensions (1-5 dimensions). Must have exactly 1 primary dimension. Additional dimensions must be non-primary. Example: Size (primary) + Color (non-primary) for t-shirts.") | ||
|
|
||
|
|
||
| class AdjustableQuantity(BaseModel): | ||
| maximum: int = Field(..., ge=1, le=100, description="Maximum quantity customers can select (1-100). Use 1 for limited edition items, higher values for bulk products. Examples: 1 for unique items, 10 for regular products, 100 for wholesale.") | ||
|
|
||
|
|
||
| class LineItem(BaseModel): | ||
| name: str = Field(..., min_length=1, max_length=127, description="The product or service name displayed to customers during checkout") | ||
| product_id: Optional[str] = Field(None, min_length=1, max_length=50, description="Your internal identifier for this product or SKU") | ||
| description: Optional[str] = Field(None, min_length=1, max_length=2048, description="Detailed information about the product or service shown to customers during checkout") | ||
| unit_amount: UnitAmount = Field(..., description="The currency and amount for this line item. IMPORTANT: If using variants with pricing in options, this field should be omitted OR set to the base price. Cannot have unit_amount at both product level and variant option level.") | ||
| taxes: Optional[List[Tax]] = Field(None, max_items=1, description="Tax configuration for this item (maximum 1). Tax is displayed separately during checkout.") | ||
| shipping: Optional[List[Shipping]] = Field(None, max_items=1, description="Shipping configuration for this item (maximum 1). Shipping fee is displayed separately during checkout.") | ||
| collect_shipping_address: Optional[bool] = Field(None, description="Set to true to prompt customers for a shipping address during checkout. Required for physical goods that need to be shipped. Default: false") | ||
| customer_notes: Optional[List[CustomerNote]] = Field(None, max_items=1, description="Custom field to collect additional information from customers about this item (maximum 1 field per item). Useful for personalization, special requests, or delivery instructions.") | ||
| variants: Optional[Variants] = Field(None, description="Product variants configuration for size, color, material, etc. IMPORTANT: If variants have unit_amount in options, do NOT include product-level unit_amount (causes validation error). If variants have NO pricing, product-level unit_amount is REQUIRED.") | ||
| adjustable_quantity: Optional[AdjustableQuantity] = Field(None, description="Enables quantity selection during checkout. Customers can choose from 1 up to the maximum. Useful for allowing bulk purchases or limiting quantities for special items.") | ||
|
|
||
|
|
||
| # Payment Link ID validation pattern | ||
| PAYMENT_LINK_ID_REGEX = r"^PLB-[A-Z0-9]{12,16}$" | ||
|
|
||
|
|
||
| class CreatePaymentLinkParameters(BaseModel): | ||
| integration_mode: str = Field(default="LINK", description="The integration mode for the payment link. Default and recommended: 'LINK'. This determines how the payment link is presented.") | ||
| type: str = Field(..., description="The type of payment link. Use 'BUY_NOW' for standard e-commerce purchases (fully supported).") | ||
| reusable: Literal['MULTIPLE'] = Field(default="MULTIPLE", description="Determines link reusability. For BUY_NOW type, must be set to 'MULTIPLE' to enable sharing and multiple uses across different customers.") | ||
| return_url: Optional[str] = Field(None, description="Optional URL to redirect customers after successful payment. Example: 'https://yoursite.com/thank-you'. If omitted, customers will stay on the PayPal default success page") | ||
| line_items: List[LineItem] = Field(..., min_items=1, description="Array of products/services in this payment link. Currently supports exactly 1 item. Each item represents a product with pricing, taxes, shipping, and optional variants.") | ||
|
|
||
|
|
||
| class ListPaymentLinksParameters(BaseModel): | ||
| """Parameters for listing payment links""" | ||
| page: Optional[int] = Field(1, ge=1, le=1000, description="Page number to retrieve (1-1000). Default: 1. Use for pagination when you have many payment links.") | ||
| page_size: Optional[int] = Field(10, ge=1, le=100, description="Number of payment links per page (1-100). Default: 10. Increase for bulk operations, decrease for faster responses.") | ||
| total_required: Optional[bool] = Field(None, description="Set to true to include total count of all payment links in response. Useful for pagination UI. Default: false (faster response).") | ||
|
|
||
|
|
||
| class GetPaymentLinkParameters(BaseModel): | ||
| """Parameters for retrieving a specific payment link""" | ||
| payment_link_id: str = Field(..., pattern=PAYMENT_LINK_ID_REGEX, description="The PayPal Payment Link ID to retrieve. Format: PLB-XXXXXXXXXXXX (PLB- prefix followed by 12-16 alphanumeric characters). Example: 'PLB-1A2B3C4D5E6F'") | ||
|
|
||
|
|
||
| class UpdatePaymentLinkParameters(BaseModel): | ||
| """Parameters for updating a payment link (full replacement)""" | ||
| payment_link_id: str = Field(..., pattern=PAYMENT_LINK_ID_REGEX, description="The PayPal Payment Link ID to update. Format: PLB-XXXXXXXXXXXX (PLB- prefix followed by 12-16 alphanumeric characters). Example: 'PLB-1A2B3C4D5E6F'") | ||
| integration_mode: str = Field(default="LINK", description="The integration mode for the payment link. Default and recommended: 'LINK'.") | ||
| type: str = Field(..., description="The type of payment link. Use 'BUY_NOW' for standard e-commerce purchases.") | ||
| reusable: Literal['MULTIPLE'] = Field(default="MULTIPLE", description="Determines link reusability. For BUY_NOW type, must be set to 'MULTIPLE' to enable sharing and multiple uses across different customers.") | ||
| return_url: Optional[str] = Field(None, description="Optional URL to redirect customers after successful payment. Updates the return URL for this payment link.") | ||
| line_items: List[LineItem] = Field(..., min_items=1, description="Complete array of line items for the payment link. This is a full replacement - all items must be provided, not just changes.") | ||
|
|
||
|
|
||
| class DeletePaymentLinkParameters(BaseModel): | ||
| """Parameters for deleting a payment link""" | ||
| payment_link_id: str = Field(..., pattern=PAYMENT_LINK_ID_REGEX, description="The PayPal Payment Link ID to delete permanently. Format: PLB-XXXXXXXXXXXX (PLB- prefix followed by 12-16 alphanumeric characters). ⚠️ WARNING: Deletion is permanent and cannot be undone.") | ||
58 changes: 58 additions & 0 deletions
58
python/paypal_agent_toolkit/shared/payment_links/prompts.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| CREATE_PAYMENT_LINK_PROMPT = """ | ||
| Create a payment link on PayPal. | ||
|
|
||
| This function creates a shareable payment link that can be sent to customers for payment collection. The payment link supports products with pricing, tax calculation, shipping fees, product variants (size, color, material), quantity controls, custom customer notes, and return URLs. | ||
|
|
||
| Important limitations to note: | ||
| - Tax types: Only PERCENTAGE and PREFERENCE are supported. FLAT tax type will return a 422 error. | ||
| - Shipping types: Only FLAT and PREFERENCE are supported. PERCENTAGE shipping type will return a 422 error. | ||
| - Reusable mode: Use "MULTIPLE" for BUY_NOW type. SINGLE mode is not supported in the current version. | ||
| - Variants: Must have exactly one primary dimension. Other dimensions must be marked as non-primary. | ||
| - Pricing: Cannot specify unit_amount at both product level and variant option level. Choose one approach. | ||
| - Type: Use "BUY_NOW" for standard purchases. | ||
|
|
||
| Once created, the link can be shared with customers via email, social media, or embedded on websites. | ||
| """ | ||
|
|
||
| LIST_PAYMENT_LINKS_PROMPT = """ | ||
| List payment links from PayPal. | ||
|
|
||
| This function retrieves a paginated list of payment links with optional filtering parameters. You can control pagination using page and page_size parameters. Set total_required to true to include the total count of payment links in the response. | ||
| """ | ||
|
|
||
| GET_PAYMENT_LINK_PROMPT = """ | ||
| Get details of a specific payment link from PayPal. | ||
|
|
||
| This function retrieves comprehensive details about a payment link using its ID. The response includes the shareable URL, current status (ACTIVE, INACTIVE, etc.), line items with pricing, taxes, shipping, variant configurations, and return URL settings. | ||
|
|
||
| Payment link ID format: PLB-XXXXXXXXXXXX (PLB- prefix followed by 12-16 alphanumeric characters). | ||
| """ | ||
|
|
||
| UPDATE_PAYMENT_LINK_PROMPT = """ | ||
| Update a payment link on PayPal. | ||
|
|
||
| This function performs a complete replacement of a payment link's configuration using a PUT operation. You must provide all required fields including payment_link_id, type, and line_items. The same limitations apply as when creating a payment link (tax types, shipping types, reusable mode, variants, and pricing rules). | ||
|
|
||
| Note: This is a full replacement operation. All fields must be provided, not just the ones you want to change. | ||
| """ | ||
|
|
||
| DELETE_PAYMENT_LINK_PROMPT = """ | ||
| Delete a payment link created through the PayPal Pay Links & Buttons API. | ||
|
|
||
| This operation deactivates the payment link so it can no longer be used by customers. | ||
| Once deleted, the shareable link URL will stop working. | ||
|
|
||
| ### Important notes | ||
| - Deletion removes the payment resource from active use. A deleted link cannot be used for future payments. | ||
| - If an invalid or non-existent payment link ID is provided, the PayPal API may return a 404 Not Found error. | ||
| - This operation does not affect completed transactions tied to the link. | ||
|
|
||
| ### Required fields | ||
| - `id`: Unique identifier of the payment link you want to delete. | ||
|
|
||
| ### Example usage | ||
| To delete a payment link: | ||
| delete_payment_link( | ||
| id="PR-1234567890" | ||
| ) | ||
| """ |
134 changes: 134 additions & 0 deletions
134
python/paypal_agent_toolkit/shared/payment_links/tool_handlers.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| from .parameters import ( | ||
| CreatePaymentLinkParameters, | ||
| ListPaymentLinksParameters, | ||
| GetPaymentLinkParameters, | ||
| UpdatePaymentLinkParameters, | ||
| DeletePaymentLinkParameters | ||
| ) | ||
| import json | ||
|
|
||
|
|
||
| def create_payment_link(client, params: dict): | ||
| """ | ||
| Create a payment link on PayPal. | ||
|
|
||
| Args: | ||
| client: PayPal client instance | ||
| params: Dictionary containing payment link parameters | ||
|
|
||
| Returns: | ||
| JSON string with payment link details including the shareable link URL | ||
| """ | ||
| validated = CreatePaymentLinkParameters(**params) | ||
| payload = validated.model_dump() | ||
|
|
||
| # Ensure required fields have defaults | ||
| if 'integration_mode' not in payload or not payload['integration_mode']: | ||
| payload['integration_mode'] = 'LINK' | ||
| if 'reusable' not in payload or not payload['reusable']: | ||
| payload['reusable'] = 'MULTIPLE' | ||
|
|
||
| url = "/v1/checkout/payment-resources" | ||
| response = client.post(uri=url, payload=payload) | ||
|
|
||
| return json.dumps(response) | ||
|
|
||
|
|
||
| def list_payment_links(client, params: dict): | ||
| """ | ||
| List payment links from PayPal. | ||
|
|
||
| Args: | ||
| client: PayPal client instance | ||
| params: Dictionary containing pagination parameters | ||
|
|
||
| Returns: | ||
| JSON string with list of payment links | ||
| """ | ||
| validated = ListPaymentLinksParameters(**params) | ||
|
|
||
| # Build query string | ||
| query_params = [] | ||
| if validated.page: | ||
| query_params.append(f"page={validated.page}") | ||
| if validated.page_size: | ||
| query_params.append(f"page_size={validated.page_size}") | ||
| if validated.total_required is not None: | ||
| query_params.append(f"total_required={str(validated.total_required).lower()}") | ||
|
|
||
| query_string = "&".join(query_params) | ||
| url = f"/v1/checkout/payment-resources?{query_string}" if query_string else "/v1/checkout/payment-resources" | ||
|
|
||
| response = client.get(uri=url) | ||
| return json.dumps(response) | ||
|
|
||
|
|
||
| def get_payment_link(client, params: dict): | ||
| """ | ||
| Get a specific payment link from PayPal. | ||
|
|
||
| Args: | ||
| client: PayPal client instance | ||
| params: Dictionary containing payment_link_id | ||
|
|
||
| Returns: | ||
| JSON string with payment link details | ||
| """ | ||
| validated = GetPaymentLinkParameters(**params) | ||
| url = f"/v1/checkout/payment-resources/{validated.payment_link_id}" | ||
|
|
||
| response = client.get(uri=url) | ||
| return json.dumps(response) | ||
|
|
||
|
|
||
| def update_payment_link(client, params: dict): | ||
| """ | ||
| Update a payment link on PayPal (full replacement). | ||
|
|
||
| Args: | ||
| client: PayPal client instance | ||
| params: Dictionary containing payment link parameters | ||
|
|
||
| Returns: | ||
| JSON string with updated payment link details | ||
| """ | ||
| validated = UpdatePaymentLinkParameters(**params) | ||
| payment_link_id = validated.payment_link_id | ||
|
|
||
| # Build payload excluding the ID | ||
| payload = validated.model_dump(exclude={'payment_link_id'}) | ||
|
|
||
| # Ensure required fields have defaults | ||
| if 'integration_mode' not in payload or not payload['integration_mode']: | ||
| payload['integration_mode'] = 'LINK' | ||
| if 'reusable' not in payload or not payload['reusable']: | ||
| payload['reusable'] = 'MULTIPLE' | ||
|
|
||
| url = f"/v1/checkout/payment-resources/{payment_link_id}" | ||
| response = client.put(uri=url, payload=payload) | ||
|
|
||
| return json.dumps(response) | ||
|
|
||
|
|
||
| def delete_payment_link(client, params: dict): | ||
| """ | ||
| Delete a payment link from PayPal. | ||
|
|
||
| Args: | ||
| client: PayPal client instance | ||
| params: Dictionary containing payment_link_id | ||
|
|
||
| Returns: | ||
| JSON string with deletion confirmation | ||
| """ | ||
| validated = DeletePaymentLinkParameters(**params) | ||
| url = f"/v1/checkout/payment-resources/{validated.payment_link_id}" | ||
|
|
||
| response = client.delete(uri=url) | ||
|
|
||
| # DELETE often returns None for 204 No Content | ||
| if response is None: | ||
| return json.dumps({"success": True, "payment_link_id": validated.payment_link_id}) | ||
|
|
||
| return json.dumps(response) | ||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove Note: 'DONATION' type has limited support and may cause validation errors.