Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Currently, the Razorpay MCP Server provides the following tools:
| `fetch_payment_card_details` | Fetch card details used for a payment | [Payment](https://razorpay.com/docs/api/payments/fetch-payment-expanded-card) | ✅ |
| `fetch_all_payments` | Fetch all payments with filtering and pagination | [Payment](https://razorpay.com/docs/api/payments/fetch-all-payments) | ✅ |
| `update_payment` | Update the notes field of a payment | [Payment](https://razorpay.com/docs/api/payments/update) | ✅ |
| `create_payment` | Create a payment using various payment methods | [Payment](https://razorpay.com/docs/api/payments/create) | ❌ |
| `create_payment_link` | Creates a new payment link (standard) | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/create-standard) | ✅ |
| `create_payment_link_upi` | Creates a new UPI payment link | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/create-upi) | ✅ |
| `fetch_all_payment_links` | Fetch all the payment links | [Payment Link](https://razorpay.com/docs/api/payments/payment-links/fetch-all-standard) | ✅ |
Expand Down Expand Up @@ -51,6 +52,9 @@ Currently, the Razorpay MCP Server provides the following tools:
| `fetch_instant_settlement_with_id` | Fetch instant settlement with ID | [Settlement](https://razorpay.com/docs/api/settlements/instant/fetch-with-id) | ✅ |
| `fetch_all_payouts` | Fetch all payout details with A/c number | [Payout](https://razorpay.com/docs/api/x/payouts/fetch-all/) | ✅ |
| `fetch_payout_by_id` | Fetch the payout details with payout ID | [Payout](https://razorpay.com/docs/api/x/payouts/fetch-with-id) | ✅ |
| `fetch_token` | Fetch token details using customer_id and token_id | [Token](https://razorpay.com/docs/api/tokens/fetch-with-id) | ✅ |
| `fetch_all_tokens` | Fetch all tokens for a specific customer | [Token](https://razorpay.com/docs/api/tokens/fetch-all) | ✅ |
| `create_customer` | Create a new customer with name and optional contact details | [Customer](https://razorpay.com/docs/api/customers) | ❌ |


## Use Cases
Expand Down
Binary file added main
Binary file not shown.
65 changes: 65 additions & 0 deletions pkg/razorpay/customer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package razorpay

import (
"context"
"fmt"

rzpsdk "github.com/razorpay/razorpay-go"

"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)

// CreateCustomer returns a tool that creates a new customer in Razorpay
func CreateCustomer(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"contact",
mcpgo.Description("Contact number (mobile number) of the customer"),
),
mcpgo.WithString(
"fail_existing",
mcpgo.Description("Set to '0' to return existing customer if already exists instead of failing"),
),
}

handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}

customerData := make(map[string]interface{})

validator := NewValidator(&r).
ValidateAndAddOptionalString(customerData, "contact").
ValidateAndAddOptionalString(customerData, "fail_existing")

if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}

// Create customer using Razorpay SDK
customer, err := client.Customer.Create(customerData, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("creating customer failed: %s", err.Error())), nil
}

return mcpgo.NewToolResultJSON(customer)
}

return mcpgo.NewTool(
"create_customer",
"Create a new customer in Razorpay with contact details and optional fail_existing flag", //nolint:lll
parameters,
handler,
)
}
192 changes: 192 additions & 0 deletions pkg/razorpay/customer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package razorpay

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/razorpay/razorpay-go/constants"

"github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
)

func Test_CreateCustomer(t *testing.T) {
createCustomerPath := fmt.Sprintf(
"/%s%s",
constants.VERSION_V1,
constants.CUSTOMER_URL,
)

successResponse := map[string]interface{}{
"id": "cust_1Aa00000000001",
"entity": "customer",
"name": "John Doe",
"email": "[email protected]",
"contact": "+919876543210",
"gstin": nil,
"notes": map[string]interface{}{
"purpose": "Test customer creation",
},
"created_at": float64(1234567890),
}

validationErrorResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "The name field is required.",
},
}

tests := []RazorpayToolTestCase{
{
Name: "successful customer creation with all parameters",
Request: map[string]interface{}{
"name": "John Doe",
"email": "[email protected]",
"contact": "+919876543210",
"notes": map[string]interface{}{
"purpose": "Test customer creation",
},
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: successResponse,
},
)
},
ExpectError: false,
ExpectedResult: successResponse,
},
{
Name: "successful customer creation with only name",
Request: map[string]interface{}{
"name": "Jane Smith",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
minimalResponse := map[string]interface{}{
"id": "cust_1Aa00000000002",
"entity": "customer",
"name": "Jane Smith",
"email": nil,
"contact": nil,
"gstin": nil,
"notes": map[string]interface{}{},
"created_at": float64(1234567891),
}
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: minimalResponse,
},
)
},
ExpectError: false,
ExpectedResult: map[string]interface{}{
"id": "cust_1Aa00000000002",
"entity": "customer",
"name": "Jane Smith",
"email": nil,
"contact": nil,
"gstin": nil,
"notes": map[string]interface{}{},
"created_at": float64(1234567891),
},
},
{
Name: "missing required name parameter",
Request: map[string]interface{}{
"email": "[email protected]",
"contact": "+919876543210",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: name",
},
{
Name: "missing all parameters",
Request: map[string]interface{}{},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: name",
},
{
Name: "invalid parameter type - name as number",
Request: map[string]interface{}{
"name": 12345,
"email": "[email protected]",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "invalid parameter type: name",
},
{
Name: "invalid parameter type - email as number",
Request: map[string]interface{}{
"name": "John Doe",
"email": 12345,
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "invalid parameter type: email",
},
{
Name: "invalid parameter type - contact as number",
Request: map[string]interface{}{
"name": "John Doe",
"contact": 12345,
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "invalid parameter type: contact",
},
{
Name: "invalid parameter type - notes as string",
Request: map[string]interface{}{
"name": "John Doe",
"notes": "invalid notes format",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "invalid parameter type: notes",
},
{
Name: "API error - validation failure",
Request: map[string]interface{}{
"name": "", // Empty name should cause API validation error
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: validationErrorResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "creating customer failed: The name field is required.",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
"name": 12345, // Wrong type
"email": true, // Wrong type
"contact": []int{1, 2, 3}, // Wrong type
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "invalid parameter type: name", // First error returned
},
}

for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, CreateCustomer, "Customer")
})
}
}
114 changes: 114 additions & 0 deletions pkg/razorpay/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,117 @@ func FetchAllPayments(
handler,
)
}

// CreatePayment returns a tool that creates a payment with wallet using the CreatePaymentJson function
func CreatePaymentWallet(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithNumber(
"amount",
mcpgo.Description("Payment amount in the smallest currency sub-unit "+
"(e.g., for ₹295, use 29500)"),
mcpgo.Required(),
mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
),
mcpgo.WithString(
"currency",
mcpgo.Description("ISO code for the currency to be taken as INR"),
mcpgo.Required(),
mcpgo.Pattern("^[A-Z]{3}$"), // ISO currency codes are 3 uppercase letters
),
mcpgo.WithString(
"method",
mcpgo.Description("Payment method to be taken as wallet"),
mcpgo.Required(),
),
mcpgo.WithString(
"wallet",
mcpgo.Description("wallet for the payment to be taken as amazonpay"),
mcpgo.Required(),
),
mcpgo.WithString(
"email",
mcpgo.Description("Customer's email address to be taken as [email protected]"),
mcpgo.Required(),
),
mcpgo.WithString(
"contact",
mcpgo.Description("Customer's contact number"),
mcpgo.Required(),
),
// Card-specific parameters (optional)

}

handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}

paymentData := make(map[string]interface{})
cardData := make(map[string]interface{})

validator := NewValidator(&r).
ValidateAndAddRequiredFloat(paymentData, "amount").
ValidateAndAddRequiredString(paymentData, "currency").
ValidateAndAddRequiredString(paymentData, "method").
ValidateAndAddRequiredString(paymentData, "wallet").
ValidateAndAddOptionalString(paymentData, "email").
ValidateAndAddOptionalString(paymentData, "contact")

if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}

// Validate card method requirements
//method := paymentData["method"].(string)
// if method == "card" {
// // Check if all required card fields are present
// requiredCardFields := []string{"token"}
// for _, field := range requiredCardFields {
// if _, exists := cardData[field]; !exists {
// return mcpgo.NewToolResultError(
// fmt.Sprintf("%s is required when method is 'card'", field)), nil
// }
// }

// // Add card data to payment data
// card := make(map[string]interface{})
// card["number"] = cardData["card_number"]
// card["expiry_month"] = cardData["card_expiry_month"]
// card["expiry_year"] = cardData["card_expiry_year"]
// card["cvv"] = cardData["card_cvv"]
// card["name"] = cardData["card_name"]

// paymentData["card"] = card
// }

// Remove card fields from the top level as they are now nested under 'card'
for key := range cardData {
delete(paymentData, key)
}

// Create payment using Razorpay SDK's CreatePaymentJson
payment, err := client.Payment.CreatePaymentJson(paymentData, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("creating payment failed: %s", err.Error())), nil
}

return mcpgo.NewToolResultJSON(payment)
}

return mcpgo.NewTool(
"create_payment",
"Create a payment using Razorpay's CreatePaymentJson function. Supports various payment methods including cards and wallets.", //nolint:lll
parameters,
handler,
)
}
Loading
Loading