This is page 2 of 4. Use http://codebase.md/razorpay/razorpay-mcp-server?page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ └── new-tool-from-docs.mdc
├── .cursorignore
├── .dockerignore
├── .github
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── pull_request_template.md
│ └── workflows
│ ├── assign.yml
│ ├── build.yml
│ ├── ci.yml
│ ├── docker-publish.yml
│ ├── lint.yml
│ └── release.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── cmd
│ └── razorpay-mcp-server
│ ├── main_test.go
│ ├── main.go
│ ├── stdio_test.go
│ └── stdio.go
├── codecov.yml
├── CONTRIBUTING.md
├── coverage.out
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── pkg
│ ├── contextkey
│ │ ├── context_key_test.go
│ │ └── context_key.go
│ ├── log
│ │ ├── config_test.go
│ │ ├── config.go
│ │ ├── log.go
│ │ ├── slog_test.go
│ │ └── slog.go
│ ├── mcpgo
│ │ ├── README.md
│ │ ├── server_test.go
│ │ ├── server.go
│ │ ├── stdio_test.go
│ │ ├── stdio.go
│ │ ├── tool_test.go
│ │ ├── tool.go
│ │ └── transport.go
│ ├── observability
│ │ ├── observability_test.go
│ │ └── observability.go
│ ├── razorpay
│ │ ├── mock
│ │ │ ├── server_test.go
│ │ │ └── server.go
│ │ ├── orders_test.go
│ │ ├── orders.go
│ │ ├── payment_links_test.go
│ │ ├── payment_links.go
│ │ ├── payments_test.go
│ │ ├── payments.go
│ │ ├── payouts_test.go
│ │ ├── payouts.go
│ │ ├── qr_codes_test.go
│ │ ├── qr_codes.go
│ │ ├── README.md
│ │ ├── refunds_test.go
│ │ ├── refunds.go
│ │ ├── server_test.go
│ │ ├── server.go
│ │ ├── settlements_test.go
│ │ ├── settlements.go
│ │ ├── test_helpers.go
│ │ ├── tokens_test.go
│ │ ├── tokens.go
│ │ ├── tools_params_test.go
│ │ ├── tools_params.go
│ │ ├── tools_test.go
│ │ └── tools.go
│ └── toolsets
│ ├── toolsets_test.go
│ └── toolsets.go
├── README.md
└── SECURITY.md
```
# Files
--------------------------------------------------------------------------------
/pkg/razorpay/orders.go:
--------------------------------------------------------------------------------
```go
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"
)
// CreateOrder returns a tool that creates new orders in Razorpay
func CreateOrder(
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 "+
"(e.g., INR, USD, SGD)"),
mcpgo.Required(),
mcpgo.Pattern("^[A-Z]{3}$"), // ISO currency codes are 3 uppercase letters
),
mcpgo.WithString(
"receipt",
mcpgo.Description("Receipt number for internal "+
"reference (max 40 chars, must be unique)"),
mcpgo.Max(40),
),
mcpgo.WithObject(
"notes",
mcpgo.Description("Key-value pairs for additional "+
"information (max 15 pairs, 256 chars each)"),
mcpgo.MaxProperties(15),
),
mcpgo.WithBoolean(
"partial_payment",
mcpgo.Description("Whether the customer can make partial payments"),
mcpgo.DefaultValue(false),
),
mcpgo.WithNumber(
"first_payment_min_amount",
mcpgo.Description("Minimum amount for first partial "+
"payment (only if partial_payment is true)"),
mcpgo.Min(100),
),
mcpgo.WithArray(
"transfers",
mcpgo.Description("Array of transfer objects for distributing "+
"payment amounts among multiple linked accounts. Each transfer "+
"object should contain: account (linked account ID), amount "+
"(in currency subunits), currency (ISO code), and optional fields "+
"like notes, linked_account_notes, on_hold, on_hold_until"),
),
mcpgo.WithString(
"method",
mcpgo.Description("Payment method for mandate orders. "+
"REQUIRED for mandate orders. Must be 'upi' when using "+
"token.type='single_block_multiple_debit'. This field is used "+
"only for mandate/recurring payment orders."),
),
mcpgo.WithString(
"customer_id",
mcpgo.Description("Customer ID for mandate orders. "+
"REQUIRED for mandate orders. Must start with 'cust_' followed by "+
"alphanumeric characters. Example: 'cust_xxx'. "+
"This identifies the customer for recurring payments."),
),
mcpgo.WithObject(
"token",
mcpgo.Description("Token object for mandate orders. "+
"REQUIRED for mandate orders. Must contain: max_amount "+
"(positive number, maximum debit amount), frequency "+
"(as_presented/monthly/one_time/yearly/weekly/daily), "+
"type='single_block_multiple_debit' (only supported type), "+
"and optionally expire_at (Unix timestamp, defaults to today+60days). "+
"Example: {\"max_amount\": 100, \"frequency\": \"as_presented\", "+
"\"type\": \"single_block_multiple_debit\"}"),
),
}
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
}
payload := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredFloat(payload, "amount").
ValidateAndAddRequiredString(payload, "currency").
ValidateAndAddOptionalString(payload, "receipt").
ValidateAndAddOptionalMap(payload, "notes").
ValidateAndAddOptionalBool(payload, "partial_payment").
ValidateAndAddOptionalArray(payload, "transfers").
ValidateAndAddOptionalString(payload, "method").
ValidateAndAddOptionalString(payload, "customer_id").
ValidateAndAddToken(payload, "token")
// Add first_payment_min_amount only if partial_payment is true
if payload["partial_payment"] == true {
validator.ValidateAndAddOptionalFloat(payload, "first_payment_min_amount")
}
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
order, err := client.Order.Create(payload, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("creating order failed: %s", err.Error()),
), nil
}
return mcpgo.NewToolResultJSON(order)
}
return mcpgo.NewTool(
"create_order",
"Create a new order in Razorpay. Supports both regular orders and "+
"mandate orders. "+
"\n\nFor REGULAR ORDERS: Provide amount, currency, and optional "+
"receipt/notes. "+
"\n\nFor MANDATE ORDERS (recurring payments): You MUST provide ALL "+
"of these fields: "+
"amount, currency, method='upi', customer_id (starts with 'cust_'), "+
"and token object. "+
"\n\nThe token object is required for mandate orders and must contain: "+
"max_amount (positive number), frequency "+
"(as_presented/monthly/one_time/yearly/weekly/daily), "+
"type='single_block_multiple_debit', and optionally expire_at "+
"(defaults to today+60days). "+
"\n\nIMPORTANT: When token.type is 'single_block_multiple_debit', "+
"the method MUST be 'upi'. "+
"\n\nExample mandate order payload: "+
`{"amount": 100, "currency": "INR", "method": "upi", `+
`"customer_id": "cust_abc123", `+
`"token": {"max_amount": 100, "frequency": "as_presented", `+
`"type": "single_block_multiple_debit"}, `+
`"receipt": "Receipt No. 1", "notes": {"key": "value"}}`,
parameters,
handler,
)
}
// FetchOrder returns a tool to fetch order details by ID
func FetchOrder(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"order_id",
mcpgo.Description("Unique identifier of the order to be retrieved"),
mcpgo.Required(),
),
}
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
}
payload := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(payload, "order_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
order, err := client.Order.Fetch(payload["order_id"].(string), nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching order failed: %s", err.Error()),
), nil
}
return mcpgo.NewToolResultJSON(order)
}
return mcpgo.NewTool(
"fetch_order",
"Fetch an order's details using its ID",
parameters,
handler,
)
}
// FetchAllOrders returns a tool to fetch all orders with optional filtering
func FetchAllOrders(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithNumber(
"count",
mcpgo.Description("Number of orders to be fetched "+
"(default: 10, max: 100)"),
mcpgo.Min(1),
mcpgo.Max(100),
),
mcpgo.WithNumber(
"skip",
mcpgo.Description("Number of orders to be skipped (default: 0)"),
mcpgo.Min(0),
),
mcpgo.WithNumber(
"from",
mcpgo.Description("Timestamp (in Unix format) from when "+
"the orders should be fetched"),
mcpgo.Min(0),
),
mcpgo.WithNumber(
"to",
mcpgo.Description("Timestamp (in Unix format) up till "+
"when orders are to be fetched"),
mcpgo.Min(0),
),
mcpgo.WithNumber(
"authorized",
mcpgo.Description("Filter orders based on payment authorization status. "+
"Values: 0 (orders with unauthorized payments), "+
"1 (orders with authorized payments)"),
mcpgo.Min(0),
mcpgo.Max(1),
),
mcpgo.WithString(
"receipt",
mcpgo.Description("Filter orders that contain the "+
"provided value for receipt"),
),
mcpgo.WithArray(
"expand",
mcpgo.Description("Used to retrieve additional information. "+
"Supported values: payments, payments.card, transfers, virtual_account"),
),
}
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
}
queryParams := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddPagination(queryParams).
ValidateAndAddOptionalInt(queryParams, "from").
ValidateAndAddOptionalInt(queryParams, "to").
ValidateAndAddOptionalInt(queryParams, "authorized").
ValidateAndAddOptionalString(queryParams, "receipt").
ValidateAndAddExpand(queryParams)
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
orders, err := client.Order.All(queryParams, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching orders failed: %s", err.Error()),
), nil
}
return mcpgo.NewToolResultJSON(orders)
}
return mcpgo.NewTool(
"fetch_all_orders",
"Fetch all orders with optional filtering and pagination",
parameters,
handler,
)
}
// FetchOrderPayments returns a tool to fetch all payments for a specific order
func FetchOrderPayments(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"order_id",
mcpgo.Description(
"Unique identifier of the order for which payments should"+
" be retrieved. Order id should start with `order_`"),
mcpgo.Required(),
),
}
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
}
orderPaymentsReq := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(orderPaymentsReq, "order_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Fetch payments for the order using Razorpay SDK
// Note: Using the Order.Payments method from SDK
orderID := orderPaymentsReq["order_id"].(string)
payments, err := client.Order.Payments(orderID, nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf(
"fetching payments for order failed: %s",
err.Error(),
),
), nil
}
// Return the result as JSON
return mcpgo.NewToolResultJSON(payments)
}
return mcpgo.NewTool(
"fetch_order_payments",
"Fetch all payments made for a specific order in Razorpay",
parameters,
handler,
)
}
// UpdateOrder returns a tool to update an order
// only the order's notes can be updated
func UpdateOrder(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"order_id",
mcpgo.Description("Unique identifier of the order which "+
"needs to be updated. ID should have an order_ prefix."),
mcpgo.Required(),
),
mcpgo.WithObject(
"notes",
mcpgo.Description("Key-value pairs used to store additional "+
"information about the order. A maximum of 15 key-value pairs "+
"can be included, with each value not exceeding 256 characters."),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
orderUpdateReq := make(map[string]interface{})
data := make(map[string]interface{})
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
validator := NewValidator(&r).
ValidateAndAddRequiredString(orderUpdateReq, "order_id").
ValidateAndAddRequiredMap(orderUpdateReq, "notes")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
data["notes"] = orderUpdateReq["notes"]
orderID := orderUpdateReq["order_id"].(string)
order, err := client.Order.Update(orderID, data, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("updating order failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(order)
}
return mcpgo.NewTool(
"update_order",
"Use this tool to update the notes for a specific order. "+
"Only the notes field can be modified.",
parameters,
handler,
)
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/tools_params.go:
--------------------------------------------------------------------------------
```go
package razorpay
import (
"encoding/json"
"errors"
"strings"
"time"
"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
)
// Validator provides a fluent interface for validating parameters
// and collecting errors
type Validator struct {
request *mcpgo.CallToolRequest
errors []error
}
// NewValidator creates a new validator for the given request
func NewValidator(r *mcpgo.CallToolRequest) *Validator {
return &Validator{
request: r,
errors: []error{},
}
}
// addError adds a non-nil error to the collection
func (v *Validator) addError(err error) *Validator {
if err != nil {
v.errors = append(v.errors, err)
}
return v
}
// HasErrors returns true if there are any validation errors
func (v *Validator) HasErrors() bool {
return len(v.errors) > 0
}
// HandleErrorsIfAny formats all errors and returns an appropriate tool result
func (v *Validator) HandleErrorsIfAny() (*mcpgo.ToolResult, error) {
if v.HasErrors() {
messages := make([]string, 0, len(v.errors))
for _, err := range v.errors {
messages = append(messages, err.Error())
}
errorMsg := "Validation errors:\n- " + strings.Join(messages, "\n- ")
return mcpgo.NewToolResultError(errorMsg), nil
}
return nil, nil
}
// extractValueGeneric is a standalone generic function to extract a parameter
// of type T
func extractValueGeneric[T any](
request *mcpgo.CallToolRequest,
name string,
required bool,
) (*T, error) {
// Type assert Arguments from any to map[string]interface{}
args, ok := request.Arguments.(map[string]interface{})
if !ok {
return nil, errors.New("invalid arguments type")
}
val, ok := args[name]
if !ok || val == nil {
if required {
return nil, errors.New("missing required parameter: " + name)
}
return nil, nil // Not an error for optional params
}
var result T
data, err := json.Marshal(val)
if err != nil {
return nil, errors.New("invalid parameter type: " + name)
}
err = json.Unmarshal(data, &result)
if err != nil {
return nil, errors.New("invalid parameter type: " + name)
}
return &result, nil
}
// Generic validation functions
// validateAndAddRequired validates and adds a required parameter of any type
func validateAndAddRequired[T any](
v *Validator,
params map[string]interface{},
name string,
) *Validator {
value, err := extractValueGeneric[T](v.request, name, true)
if err != nil {
return v.addError(err)
}
if value == nil {
return v
}
params[name] = *value
return v
}
// validateAndAddOptional validates and adds an optional parameter of any type
// if not empty
func validateAndAddOptional[T any](
v *Validator,
params map[string]interface{},
name string,
) *Validator {
value, err := extractValueGeneric[T](v.request, name, false)
if err != nil {
return v.addError(err)
}
if value == nil {
return v
}
params[name] = *value
return v
}
// validateAndAddToPath is a generic helper to extract a value and write it into
// `target[targetKey]` if non-empty
func validateAndAddToPath[T any](
v *Validator,
target map[string]interface{},
paramName string,
targetKey string,
) *Validator {
value, err := extractValueGeneric[T](v.request, paramName, false)
if err != nil {
return v.addError(err)
}
if value == nil {
return v
}
target[targetKey] = *value
return v
}
// ValidateAndAddOptionalStringToPath validates an optional string
// and writes it into target[targetKey]
func (v *Validator) ValidateAndAddOptionalStringToPath(
target map[string]interface{},
paramName, targetKey string,
) *Validator {
return validateAndAddToPath[string](v, target, paramName, targetKey) // nolint:lll
}
// ValidateAndAddOptionalBoolToPath validates an optional bool
// and writes it into target[targetKey]
// only if it was explicitly provided in the request
func (v *Validator) ValidateAndAddOptionalBoolToPath(
target map[string]interface{},
paramName, targetKey string,
) *Validator {
// Now validate and add the parameter
value, err := extractValueGeneric[bool](v.request, paramName, false)
if err != nil {
return v.addError(err)
}
if value == nil {
return v
}
target[targetKey] = *value
return v
}
// ValidateAndAddOptionalIntToPath validates an optional integer
// and writes it into target[targetKey]
func (v *Validator) ValidateAndAddOptionalIntToPath(
target map[string]interface{},
paramName, targetKey string,
) *Validator {
return validateAndAddToPath[int64](v, target, paramName, targetKey)
}
// Type-specific validator methods
// ValidateAndAddRequiredString validates and adds a required string parameter
func (v *Validator) ValidateAndAddRequiredString(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddRequired[string](v, params, name)
}
// ValidateAndAddOptionalString validates and adds an optional string parameter
func (v *Validator) ValidateAndAddOptionalString(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddOptional[string](v, params, name)
}
// ValidateAndAddRequiredMap validates and adds a required map parameter
func (v *Validator) ValidateAndAddRequiredMap(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddRequired[map[string]interface{}](v, params, name)
}
// ValidateAndAddOptionalMap validates and adds an optional map parameter
func (v *Validator) ValidateAndAddOptionalMap(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddOptional[map[string]interface{}](v, params, name)
}
// ValidateAndAddRequiredArray validates and adds a required array parameter
func (v *Validator) ValidateAndAddRequiredArray(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddRequired[[]interface{}](v, params, name)
}
// ValidateAndAddOptionalArray validates and adds an optional array parameter
func (v *Validator) ValidateAndAddOptionalArray(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddOptional[[]interface{}](v, params, name)
}
// ValidateAndAddPagination validates and adds pagination parameters
// (count and skip)
func (v *Validator) ValidateAndAddPagination(
params map[string]interface{},
) *Validator {
return v.ValidateAndAddOptionalInt(params, "count").
ValidateAndAddOptionalInt(params, "skip")
}
// ValidateAndAddExpand validates and adds expand parameters
func (v *Validator) ValidateAndAddExpand(
params map[string]interface{},
) *Validator {
expand, err := extractValueGeneric[[]string](v.request, "expand", false)
if err != nil {
return v.addError(err)
}
if expand == nil {
return v
}
if len(*expand) > 0 {
for _, val := range *expand {
params["expand[]"] = val
}
}
return v
}
// ValidateAndAddRequiredInt validates and adds a required integer parameter
func (v *Validator) ValidateAndAddRequiredInt(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddRequired[int64](v, params, name)
}
// ValidateAndAddOptionalInt validates and adds an optional integer parameter
func (v *Validator) ValidateAndAddOptionalInt(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddOptional[int64](v, params, name)
}
// ValidateAndAddRequiredFloat validates and adds a required float parameter
func (v *Validator) ValidateAndAddRequiredFloat(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddRequired[float64](v, params, name)
}
// ValidateAndAddOptionalFloat validates and adds an optional float parameter
func (v *Validator) ValidateAndAddOptionalFloat(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddOptional[float64](v, params, name)
}
// ValidateAndAddRequiredBool validates and adds a required boolean parameter
func (v *Validator) ValidateAndAddRequiredBool(
params map[string]interface{},
name string,
) *Validator {
return validateAndAddRequired[bool](v, params, name)
}
// ValidateAndAddOptionalBool validates and adds an optional boolean parameter
// Note: This adds the boolean value only
// if it was explicitly provided in the request
func (v *Validator) ValidateAndAddOptionalBool(
params map[string]interface{},
name string,
) *Validator {
// Now validate and add the parameter
value, err := extractValueGeneric[bool](v.request, name, false)
if err != nil {
return v.addError(err)
}
if value == nil {
return v
}
params[name] = *value
return v
}
// validateTokenMaxAmount validates the max_amount field in token.
// max_amount is required and must be a positive number representing
// the maximum amount that can be debited from the customer's account.
func (v *Validator) validateTokenMaxAmount(
token map[string]interface{}) *Validator {
if maxAmount, exists := token["max_amount"]; exists {
switch amt := maxAmount.(type) {
case float64:
if amt <= 0 {
return v.addError(errors.New("token.max_amount must be greater than 0"))
}
case int:
if amt <= 0 {
return v.addError(errors.New("token.max_amount must be greater than 0"))
}
token["max_amount"] = float64(amt) // Convert int to float64
default:
return v.addError(errors.New("token.max_amount must be a number"))
}
} else {
return v.addError(errors.New("token.max_amount is required"))
}
return v
}
// validateTokenExpireAt validates the expire_at field in token.
// expire_at is optional and defaults to today + 60 days if not provided.
// If provided, it must be a positive Unix timestamp indicating when the
// mandate/token should expire.
func (v *Validator) validateTokenExpireAt(
token map[string]interface{}) *Validator {
if expireAt, exists := token["expire_at"]; exists {
switch exp := expireAt.(type) {
case float64:
if exp <= 0 {
return v.addError(errors.New("token.expire_at must be greater than 0"))
}
case int:
if exp <= 0 {
return v.addError(errors.New("token.expire_at must be greater than 0"))
}
token["expire_at"] = float64(exp) // Convert int to float64
default:
return v.addError(errors.New("token.expire_at must be a number"))
}
} else {
// Set default value to today + 60 days
defaultExpireAt := time.Now().AddDate(0, 0, 60).Unix()
token["expire_at"] = float64(defaultExpireAt)
}
return v
}
// validateTokenFrequency validates the frequency field in token.
// frequency is required and must be one of the allowed values:
// "as_presented", "monthly", "one_time", "yearly", "weekly", "daily".
func (v *Validator) validateTokenFrequency(
token map[string]interface{}) *Validator {
if frequency, exists := token["frequency"]; exists {
if freqStr, ok := frequency.(string); ok {
validFrequencies := []string{
"as_presented", "monthly", "one_time", "yearly", "weekly", "daily"}
for _, validFreq := range validFrequencies {
if freqStr == validFreq {
return v
}
}
return v.addError(errors.New(
"token.frequency must be one of: as_presented, " +
"monthly, one_time, yearly, weekly, daily"))
}
return v.addError(errors.New("token.frequency must be a string"))
}
return v.addError(errors.New("token.frequency is required"))
}
// validateTokenType validates the type field in token.
// type is required and must be "single_block_multiple_debit" for SBMD mandates.
func (v *Validator) validateTokenType(token map[string]interface{}) *Validator {
if tokenType, exists := token["type"]; exists {
if typeStr, ok := tokenType.(string); ok {
validTypes := []string{"single_block_multiple_debit"}
for _, validType := range validTypes {
if typeStr == validType {
return v
}
}
return v.addError(errors.New(
"token.type must be one of: single_block_multiple_debit"))
}
return v.addError(errors.New("token.type must be a string"))
}
return v.addError(errors.New("token.type is required"))
}
// ValidateAndAddToken validates and adds a token object with proper structure.
// The token object is used for mandate orders and must contain:
// - max_amount: positive number (maximum debit amount)
// - expire_at: optional Unix timestamp (mandate expiry,
// defaults to today + 60 days)
// - frequency: string (debit frequency: as_presented, monthly, one_time,
// yearly, weekly, daily)
// - type: string (mandate type: single_block_multiple_debit)
func (v *Validator) ValidateAndAddToken(
params map[string]interface{}, name string) *Validator {
value, err := extractValueGeneric[map[string]interface{}](
v.request, name, false)
if err != nil {
return v.addError(err)
}
if value == nil {
return v
}
token := *value
// Validate all token fields
v.validateTokenMaxAmount(token).
validateTokenExpireAt(token).
validateTokenFrequency(token).
validateTokenType(token)
if v.HasErrors() {
return v
}
params[name] = token
return v
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/qr_codes.go:
--------------------------------------------------------------------------------
```go
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"
)
// CreateQRCode returns a tool that creates QR codes in Razorpay
func CreateQRCode(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"type",
mcpgo.Description(
"The type of the QR Code. Currently only supports 'upi_qr'",
),
mcpgo.Required(),
mcpgo.Pattern("^upi_qr$"),
),
mcpgo.WithString(
"name",
mcpgo.Description(
"Label to identify the QR Code (e.g., 'Store Front Display')",
),
),
mcpgo.WithString(
"usage",
mcpgo.Description(
"Whether QR should accept single or multiple payments. "+
"Possible values: 'single_use', 'multiple_use'",
),
mcpgo.Required(),
mcpgo.Enum("single_use", "multiple_use"),
),
mcpgo.WithBoolean(
"fixed_amount",
mcpgo.Description(
"Whether QR should accept only specific amount (true) or any "+
"amount (false)",
),
mcpgo.DefaultValue(false),
),
mcpgo.WithNumber(
"payment_amount",
mcpgo.Description(
"The specific amount allowed for transaction in smallest "+
"currency unit",
),
mcpgo.Min(1),
),
mcpgo.WithString(
"description",
mcpgo.Description("A brief description about the QR Code"),
),
mcpgo.WithString(
"customer_id",
mcpgo.Description(
"The unique identifier of the customer to link with the QR Code",
),
),
mcpgo.WithNumber(
"close_by",
mcpgo.Description(
"Unix timestamp at which QR Code should be automatically "+
"closed (min 2 mins after current time)",
),
),
mcpgo.WithObject(
"notes",
mcpgo.Description(
"Key-value pairs for additional information "+
"(max 15 pairs, 256 chars each)",
),
mcpgo.MaxProperties(15),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
qrData := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(qrData, "type").
ValidateAndAddRequiredString(qrData, "usage").
ValidateAndAddOptionalString(qrData, "name").
ValidateAndAddOptionalBool(qrData, "fixed_amount").
ValidateAndAddOptionalFloat(qrData, "payment_amount").
ValidateAndAddOptionalString(qrData, "description").
ValidateAndAddOptionalString(qrData, "customer_id").
ValidateAndAddOptionalFloat(qrData, "close_by").
ValidateAndAddOptionalMap(qrData, "notes")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Check if fixed_amount is true, then payment_amount is required
if fixedAmount, exists := qrData["fixed_amount"]; exists &&
fixedAmount.(bool) {
if _, exists := qrData["payment_amount"]; !exists {
return mcpgo.NewToolResultError(
"payment_amount is required when fixed_amount is true"), nil
}
}
// Create QR code using Razorpay SDK
qrCode, err := client.QrCode.Create(qrData, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("creating QR code failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(qrCode)
}
return mcpgo.NewTool(
"create_qr_code",
"Create a new QR code in Razorpay that can be used to accept UPI payments",
parameters,
handler,
)
}
// FetchQRCode returns a tool that fetches a specific QR code by ID
func FetchQRCode(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"qr_code_id",
mcpgo.Description(
"Unique identifier of the QR Code to be retrieved"+
"The QR code id should start with 'qr_'",
),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "qr_code_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
qrCodeID := params["qr_code_id"].(string)
// Fetch QR code by ID using Razorpay SDK
qrCode, err := client.QrCode.Fetch(qrCodeID, nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching QR code failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(qrCode)
}
return mcpgo.NewTool(
"fetch_qr_code",
"Fetch a QR code's details using it's ID",
parameters,
handler,
)
}
// FetchAllQRCodes returns a tool that fetches all QR codes
// with pagination support
func FetchAllQRCodes(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithNumber(
"from",
mcpgo.Description(
"Unix timestamp, in seconds, from when QR Codes are to be retrieved",
),
mcpgo.Min(0),
),
mcpgo.WithNumber(
"to",
mcpgo.Description(
"Unix timestamp, in seconds, till when QR Codes are to be retrieved",
),
mcpgo.Min(0),
),
mcpgo.WithNumber(
"count",
mcpgo.Description(
"Number of QR Codes to be retrieved (default: 10, max: 100)",
),
mcpgo.Min(1),
mcpgo.Max(100),
),
mcpgo.WithNumber(
"skip",
mcpgo.Description(
"Number of QR Codes to be skipped (default: 0)",
),
mcpgo.Min(0),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
fetchQROptions := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddOptionalInt(fetchQROptions, "from").
ValidateAndAddOptionalInt(fetchQROptions, "to").
ValidateAndAddPagination(fetchQROptions)
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Fetch QR codes using Razorpay SDK
qrCodes, err := client.QrCode.All(fetchQROptions, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(qrCodes)
}
return mcpgo.NewTool(
"fetch_all_qr_codes",
"Fetch all QR codes with optional filtering and pagination",
parameters,
handler,
)
}
// FetchQRCodesByCustomerID returns a tool that fetches QR codes
// for a specific customer ID
func FetchQRCodesByCustomerID(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"customer_id",
mcpgo.Description(
"The unique identifier of the customer",
),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
fetchQROptions := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(fetchQROptions, "customer_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Fetch QR codes by customer ID using Razorpay SDK
qrCodes, err := client.QrCode.All(fetchQROptions, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(qrCodes)
}
return mcpgo.NewTool(
"fetch_qr_codes_by_customer_id",
"Fetch all QR codes for a specific customer",
parameters,
handler,
)
}
// FetchQRCodesByPaymentID returns a tool that fetches QR codes
// for a specific payment ID
func FetchQRCodesByPaymentID(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_id",
mcpgo.Description(
"The unique identifier of the payment"+
"The payment id always should start with 'pay_'",
),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
fetchQROptions := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(fetchQROptions, "payment_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Fetch QR codes by payment ID using Razorpay SDK
qrCodes, err := client.QrCode.All(fetchQROptions, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(qrCodes)
}
return mcpgo.NewTool(
"fetch_qr_codes_by_payment_id",
"Fetch all QR codes for a specific payment",
parameters,
handler,
)
}
// FetchPaymentsForQRCode returns a tool that fetches payments made on a QR code
func FetchPaymentsForQRCode(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"qr_code_id",
mcpgo.Description(
"The unique identifier of the QR Code to fetch payments for"+
"The QR code id should start with 'qr_'",
),
mcpgo.Required(),
),
mcpgo.WithNumber(
"from",
mcpgo.Description(
"Unix timestamp, in seconds, from when payments are to be retrieved",
),
mcpgo.Min(0),
),
mcpgo.WithNumber(
"to",
mcpgo.Description(
"Unix timestamp, in seconds, till when payments are to be fetched",
),
mcpgo.Min(0),
),
mcpgo.WithNumber(
"count",
mcpgo.Description(
"Number of payments to be fetched (default: 10, max: 100)",
),
mcpgo.Min(1),
mcpgo.Max(100),
),
mcpgo.WithNumber(
"skip",
mcpgo.Description(
"Number of records to be skipped while fetching the payments",
),
mcpgo.Min(0),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
fetchQROptions := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "qr_code_id").
ValidateAndAddOptionalInt(fetchQROptions, "from").
ValidateAndAddOptionalInt(fetchQROptions, "to").
ValidateAndAddOptionalInt(fetchQROptions, "count").
ValidateAndAddOptionalInt(fetchQROptions, "skip")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
qrCodeID := params["qr_code_id"].(string)
// Fetch payments for QR code using Razorpay SDK
payments, err := client.QrCode.FetchPayments(qrCodeID, fetchQROptions, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching payments for QR code failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(payments)
}
return mcpgo.NewTool(
"fetch_payments_for_qr_code",
"Fetch all payments made on a QR code",
parameters,
handler,
)
}
// CloseQRCode returns a tool that closes a specific QR code
func CloseQRCode(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"qr_code_id",
mcpgo.Description(
"Unique identifier of the QR Code to be closed"+
"The QR code id should start with 'qr_'",
),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "qr_code_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
qrCodeID := params["qr_code_id"].(string)
// Close QR code by ID using Razorpay SDK
qrCode, err := client.QrCode.Close(qrCodeID, nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("closing QR code failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(qrCode)
}
return mcpgo.NewTool(
"close_qr_code",
"Close a QR Code that's no longer needed",
parameters,
handler,
)
}
```
--------------------------------------------------------------------------------
/pkg/mcpgo/tool.go:
--------------------------------------------------------------------------------
```go
package mcpgo
import (
"context"
"encoding/json"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
// ToolHandler handles tool calls
type ToolHandler func(
ctx context.Context,
request CallToolRequest) (*ToolResult, error)
// CallToolRequest represents a request to call a tool
type CallToolRequest struct {
Name string
Arguments any
}
// ToolResult represents the result of a tool call
type ToolResult struct {
Text string
IsError bool
Content []interface{}
}
// Tool represents a tool that can be added to the server
type Tool interface {
// internal method to convert to mcp's ServerTool
toMCPServerTool() server.ServerTool
// GetHandler internal method for fetching the underlying handler
GetHandler() ToolHandler
}
// PropertyOption represents a customization option for
// a parameter's schema
type PropertyOption func(schema map[string]interface{})
// Min sets the minimum value for a number parameter or
// minimum length for a string
func Min(value float64) PropertyOption {
return func(schema map[string]interface{}) {
propType, ok := schema["type"].(string)
if !ok {
return
}
switch propType {
case "number", "integer":
schema["minimum"] = value
case "string":
schema["minLength"] = int(value)
case "array":
schema["minItems"] = int(value)
}
}
}
// Max sets the maximum value for a number parameter or
// maximum length for a string
func Max(value float64) PropertyOption {
return func(schema map[string]interface{}) {
propType, ok := schema["type"].(string)
if !ok {
return
}
switch propType {
case "number", "integer":
schema["maximum"] = value
case "string":
schema["maxLength"] = int(value)
case "array":
schema["maxItems"] = int(value)
}
}
}
// Pattern sets a regex pattern for string validation
func Pattern(pattern string) PropertyOption {
return func(schema map[string]interface{}) {
propType, ok := schema["type"].(string)
if !ok || propType != "string" {
return
}
schema["pattern"] = pattern
}
}
// Enum sets allowed values for a parameter
func Enum(values ...interface{}) PropertyOption {
return func(schema map[string]interface{}) {
schema["enum"] = values
}
}
// DefaultValue sets a default value for a parameter
func DefaultValue(value interface{}) PropertyOption {
return func(schema map[string]interface{}) {
schema["default"] = value
}
}
// MaxProperties sets the maximum number of properties for an object
func MaxProperties(max int) PropertyOption {
return func(schema map[string]interface{}) {
propType, ok := schema["type"].(string)
if !ok || propType != "object" {
return
}
schema["maxProperties"] = max
}
}
// MinProperties sets the minimum number of properties for an object
func MinProperties(min int) PropertyOption {
return func(schema map[string]interface{}) {
propType, ok := schema["type"].(string)
if !ok || propType != "object" {
return
}
schema["minProperties"] = min
}
}
// Required sets the tool parameter as required.
// When a parameter is marked as required, the client must provide a value
// for this parameter or the tool call will fail with an error.
func Required() PropertyOption {
return func(schema map[string]interface{}) {
schema["required"] = true
}
}
// Description sets the description for the tool parameter.
// The description should explain the purpose of the parameter, expected format,
// and any relevant constraints.
func Description(desc string) PropertyOption {
return func(schema map[string]interface{}) {
schema["description"] = desc
}
}
// ToolParameter represents a parameter for a tool
type ToolParameter struct {
Name string
Schema map[string]interface{}
}
// applyPropertyOptions applies the given property options to
// the parameter schema
func (p *ToolParameter) applyPropertyOptions(opts ...PropertyOption) {
for _, opt := range opts {
opt(p.Schema)
}
}
// WithString creates a string parameter with optional property options
func WithString(name string, opts ...PropertyOption) ToolParameter {
param := ToolParameter{
Name: name,
Schema: map[string]interface{}{"type": "string"},
}
param.applyPropertyOptions(opts...)
return param
}
// WithNumber creates a number parameter with optional property options
func WithNumber(name string, opts ...PropertyOption) ToolParameter {
param := ToolParameter{
Name: name,
Schema: map[string]interface{}{"type": "number"},
}
param.applyPropertyOptions(opts...)
return param
}
// WithBoolean creates a boolean parameter with optional property options
func WithBoolean(name string, opts ...PropertyOption) ToolParameter {
param := ToolParameter{
Name: name,
Schema: map[string]interface{}{"type": "boolean"},
}
param.applyPropertyOptions(opts...)
return param
}
// WithObject creates an object parameter with optional property options
func WithObject(name string, opts ...PropertyOption) ToolParameter {
param := ToolParameter{
Name: name,
Schema: map[string]interface{}{"type": "object"},
}
param.applyPropertyOptions(opts...)
return param
}
// WithArray creates an array parameter with optional property options
func WithArray(name string, opts ...PropertyOption) ToolParameter {
param := ToolParameter{
Name: name,
Schema: map[string]interface{}{"type": "array"},
}
param.applyPropertyOptions(opts...)
return param
}
// mark3labsToolImpl implements the Tool interface
type mark3labsToolImpl struct {
name string
description string
handler ToolHandler
parameters []ToolParameter
}
// NewTool creates a new tool with the given
// Name, description, parameters and handler
func NewTool(
name,
description string,
parameters []ToolParameter,
handler ToolHandler) *mark3labsToolImpl {
return &mark3labsToolImpl{
name: name,
description: description,
handler: handler,
parameters: parameters,
}
}
// addNumberPropertyOptions adds number-specific options to the property options
func addNumberPropertyOptions(
propOpts []mcp.PropertyOption,
schema map[string]interface{}) []mcp.PropertyOption {
// Add minimum if present
if min, ok := schema["minimum"].(float64); ok {
propOpts = append(propOpts, mcp.Min(min))
}
// Add maximum if present
if max, ok := schema["maximum"].(float64); ok {
propOpts = append(propOpts, mcp.Max(max))
}
return propOpts
}
// addStringPropertyOptions adds string-specific options to the property options
func addStringPropertyOptions(
propOpts []mcp.PropertyOption,
schema map[string]interface{}) []mcp.PropertyOption {
// Add minLength if present
if minLength, ok := schema["minLength"].(int); ok {
propOpts = append(propOpts, mcp.MinLength(minLength))
}
// Add maxLength if present
if maxLength, ok := schema["maxLength"].(int); ok {
propOpts = append(propOpts, mcp.MaxLength(maxLength))
}
// Add pattern if present
if pattern, ok := schema["pattern"].(string); ok {
propOpts = append(propOpts, mcp.Pattern(pattern))
}
return propOpts
}
// addDefaultValueOptions adds default value options based on type
func addDefaultValueOptions(
propOpts []mcp.PropertyOption,
defaultValue interface{}) []mcp.PropertyOption {
switch val := defaultValue.(type) {
case string:
propOpts = append(propOpts, mcp.DefaultString(val))
case float64:
propOpts = append(propOpts, mcp.DefaultNumber(val))
case bool:
propOpts = append(propOpts, mcp.DefaultBool(val))
}
return propOpts
}
// addEnumOptions adds enum options if present
func addEnumOptions(
propOpts []mcp.PropertyOption,
enumValues interface{}) []mcp.PropertyOption {
values, ok := enumValues.([]interface{})
if !ok {
return propOpts
}
// Convert values to strings for now
strValues := make([]string, 0, len(values))
for _, ev := range values {
if str, ok := ev.(string); ok {
strValues = append(strValues, str)
}
}
if len(strValues) > 0 {
propOpts = append(propOpts, mcp.Enum(strValues...))
}
return propOpts
}
// addObjectPropertyOptions adds object-specific options
func addObjectPropertyOptions(
propOpts []mcp.PropertyOption,
schema map[string]interface{}) []mcp.PropertyOption {
// Add maxProperties if present
if maxProps, ok := schema["maxProperties"].(int); ok {
propOpts = append(propOpts, mcp.MaxProperties(maxProps))
}
// Add minProperties if present
if minProps, ok := schema["minProperties"].(int); ok {
propOpts = append(propOpts, mcp.MinProperties(minProps))
}
return propOpts
}
// addArrayPropertyOptions adds array-specific options
func addArrayPropertyOptions(
propOpts []mcp.PropertyOption,
schema map[string]interface{}) []mcp.PropertyOption {
// Add minItems if present
if minItems, ok := schema["minItems"].(int); ok {
propOpts = append(propOpts, mcp.MinItems(minItems))
}
// Add maxItems if present
if maxItems, ok := schema["maxItems"].(int); ok {
propOpts = append(propOpts, mcp.MaxItems(maxItems))
}
return propOpts
}
// convertSchemaToPropertyOptions converts our schema to mcp property options
func convertSchemaToPropertyOptions(
schema map[string]interface{}) []mcp.PropertyOption {
var propOpts []mcp.PropertyOption
// Add description if present
if description, ok := schema["description"].(string); ok && description != "" {
propOpts = append(propOpts, mcp.Description(description))
}
// Add required flag if present
if required, ok := schema["required"].(bool); ok && required {
propOpts = append(propOpts, mcp.Required())
}
// Skip type, description and required as they're handled separately
for k, v := range schema {
if k == "type" || k == "description" || k == "required" {
continue
}
// Process property based on key
switch k {
case "minimum", "maximum":
propOpts = addNumberPropertyOptions(propOpts, schema)
case "minLength", "maxLength", "pattern":
propOpts = addStringPropertyOptions(propOpts, schema)
case "default":
propOpts = addDefaultValueOptions(propOpts, v)
case "enum":
propOpts = addEnumOptions(propOpts, v)
case "maxProperties", "minProperties":
propOpts = addObjectPropertyOptions(propOpts, schema)
case "minItems", "maxItems":
propOpts = addArrayPropertyOptions(propOpts, schema)
}
}
return propOpts
}
// GetHandler returns the handler for the tool
func (t *mark3labsToolImpl) GetHandler() ToolHandler {
return t.handler
}
// toMCPServerTool converts our Tool to mcp's ServerTool
func (t *mark3labsToolImpl) toMCPServerTool() server.ServerTool {
// Create the mcp tool with appropriate options
var toolOpts []mcp.ToolOption
// Add description
toolOpts = append(toolOpts, mcp.WithDescription(t.description))
// Add parameters with their schemas
for _, param := range t.parameters {
// Get property options from schema
propOpts := convertSchemaToPropertyOptions(param.Schema)
// Get the type from the schema
schemaType, ok := param.Schema["type"].(string)
if !ok {
// Default to string if type is missing or not a string
schemaType = "string"
}
// Use the appropriate function based on schema type
switch schemaType {
case "string":
toolOpts = append(toolOpts, mcp.WithString(param.Name, propOpts...))
case "number", "integer":
toolOpts = append(toolOpts, mcp.WithNumber(param.Name, propOpts...))
case "boolean":
toolOpts = append(toolOpts, mcp.WithBoolean(param.Name, propOpts...))
case "object":
toolOpts = append(toolOpts, mcp.WithObject(param.Name, propOpts...))
case "array":
toolOpts = append(toolOpts, mcp.WithArray(param.Name, propOpts...))
default:
// Unknown type, default to string
toolOpts = append(toolOpts, mcp.WithString(param.Name, propOpts...))
}
}
// Create the tool with all options
tool := mcp.NewTool(t.name, toolOpts...)
// Create the handler
handlerFunc := func(
ctx context.Context,
req mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
// Convert mcp request to our request
ourReq := CallToolRequest{
Name: req.Params.Name,
Arguments: req.Params.Arguments,
}
// Call our handler
result, err := t.handler(ctx, ourReq)
if err != nil {
return nil, err
}
// Convert our result to mcp result
var mcpResult *mcp.CallToolResult
if result.IsError {
mcpResult = mcp.NewToolResultError(result.Text)
} else {
mcpResult = mcp.NewToolResultText(result.Text)
}
return mcpResult, nil
}
return server.ServerTool{
Tool: tool,
Handler: handlerFunc,
}
}
// NewToolResultJSON creates a new tool result with JSON content
func NewToolResultJSON(data interface{}) (*ToolResult, error) {
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
return &ToolResult{
Text: string(jsonBytes),
IsError: false,
Content: nil,
}, nil
}
// NewToolResultText creates a new tool result with text content
func NewToolResultText(text string) *ToolResult {
return &ToolResult{
Text: text,
IsError: false,
Content: nil,
}
}
// NewToolResultError creates a new tool result with an error
func NewToolResultError(text string) *ToolResult {
return &ToolResult{
Text: text,
IsError: true,
Content: nil,
}
}
```
--------------------------------------------------------------------------------
/pkg/toolsets/toolsets_test.go:
--------------------------------------------------------------------------------
```go
package toolsets
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
)
// mockServer is a mock implementation of mcpgo.Server for testing
type mockServer struct {
tools []mcpgo.Tool
}
func (m *mockServer) AddTools(tools ...mcpgo.Tool) {
m.tools = append(m.tools, tools...)
}
func (m *mockServer) GetTools() []mcpgo.Tool {
return m.tools
}
func TestNewToolset(t *testing.T) {
t.Run("creates toolset with name and description", func(t *testing.T) {
ts := NewToolset("test-toolset", "Test description")
assert.NotNil(t, ts)
assert.Equal(t, "test-toolset", ts.Name)
assert.Equal(t, "Test description", ts.Description)
assert.False(t, ts.Enabled)
assert.False(t, ts.readOnly)
})
t.Run("creates toolset with empty name", func(t *testing.T) {
ts := NewToolset("", "Description")
assert.NotNil(t, ts)
assert.Equal(t, "", ts.Name)
assert.Equal(t, "Description", ts.Description)
})
}
func TestNewToolsetGroup(t *testing.T) {
t.Run("creates toolset group with readOnly false", func(t *testing.T) {
tg := NewToolsetGroup(false)
assert.NotNil(t, tg)
assert.NotNil(t, tg.Toolsets)
assert.False(t, tg.everythingOn)
assert.False(t, tg.readOnly)
})
t.Run("creates toolset group with readOnly true", func(t *testing.T) {
tg := NewToolsetGroup(true)
assert.NotNil(t, tg)
assert.NotNil(t, tg.Toolsets)
assert.False(t, tg.everythingOn)
assert.True(t, tg.readOnly)
})
}
func TestToolset_AddWriteTools(t *testing.T) {
t.Run("adds write tools when not readOnly", func(t *testing.T) {
ts := NewToolset("test", "Test")
tool1 := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result1"), nil
})
tool2 := mcpgo.NewTool("tool2", "Tool 2", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result2"), nil
})
result := ts.AddWriteTools(tool1, tool2)
assert.Equal(t, ts, result) // Should return self for chaining
assert.Len(t, ts.writeTools, 2)
})
t.Run("does not add write tools when readOnly", func(t *testing.T) {
ts := NewToolset("test", "Test")
ts.readOnly = true
tool := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
result := ts.AddWriteTools(tool)
assert.Equal(t, ts, result)
assert.Len(t, ts.writeTools, 0) // Should not add when readOnly
})
t.Run("adds multiple write tools", func(t *testing.T) {
ts := NewToolset("test", "Test")
tool1 := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
tool2 := mcpgo.NewTool("tool2", "Tool 2", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
tool3 := mcpgo.NewTool("tool3", "Tool 3", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
ts.AddWriteTools(tool1, tool2, tool3)
assert.Len(t, ts.writeTools, 3)
})
t.Run("adds empty write tools list", func(t *testing.T) {
ts := NewToolset("test", "Test")
ts.AddWriteTools()
assert.Len(t, ts.writeTools, 0)
})
}
func TestToolset_AddReadTools(t *testing.T) {
t.Run("adds read tools", func(t *testing.T) {
ts := NewToolset("test", "Test")
tool1 := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result1"), nil
})
tool2 := mcpgo.NewTool("tool2", "Tool 2", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result2"), nil
})
result := ts.AddReadTools(tool1, tool2)
assert.Equal(t, ts, result) // Should return self for chaining
assert.Len(t, ts.readTools, 2)
})
t.Run("adds read tools even when readOnly", func(t *testing.T) {
ts := NewToolset("test", "Test")
ts.readOnly = true
tool := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
ts.AddReadTools(tool)
assert.Len(t, ts.readTools, 1) // Should add even when readOnly
})
t.Run("adds multiple read tools", func(t *testing.T) {
ts := NewToolset("test", "Test")
tool1 := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
tool2 := mcpgo.NewTool("tool2", "Tool 2", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
tool3 := mcpgo.NewTool("tool3", "Tool 3", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
ts.AddReadTools(tool1, tool2, tool3)
assert.Len(t, ts.readTools, 3)
})
t.Run("adds empty read tools list", func(t *testing.T) {
ts := NewToolset("test", "Test")
ts.AddReadTools()
assert.Len(t, ts.readTools, 0)
})
}
func TestToolset_RegisterTools(t *testing.T) {
t.Run("registers tools when enabled", func(t *testing.T) {
ts := NewToolset("test", "Test")
ts.Enabled = true
readTool := mcpgo.NewTool("read-tool", "Read Tool", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
writeTool := mcpgo.NewTool(
"write-tool", "Write Tool", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
ts.AddReadTools(readTool)
ts.AddWriteTools(writeTool)
mockSrv := &mockServer{}
ts.RegisterTools(mockSrv)
// Both read and write tools should be registered
assert.Len(t, mockSrv.GetTools(), 2)
})
t.Run("does not register tools when disabled", func(t *testing.T) {
ts := NewToolset("test", "Test")
ts.Enabled = false
tool := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
ts.AddReadTools(tool)
mockSrv := &mockServer{}
ts.RegisterTools(mockSrv)
assert.Len(t, mockSrv.GetTools(), 0) // Should not register when disabled
})
t.Run("registers only read tools when readOnly", func(t *testing.T) {
ts := NewToolset("test", "Test")
ts.Enabled = true
ts.readOnly = true
readTool := mcpgo.NewTool("read-tool", "Read Tool", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
writeTool := mcpgo.NewTool(
"write-tool", "Write Tool", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result"), nil
})
ts.AddReadTools(readTool)
ts.AddWriteTools(writeTool) // This won't add because readOnly
mockSrv := &mockServer{}
ts.RegisterTools(mockSrv)
assert.Len(t, mockSrv.GetTools(), 1) // Only read tool should be registered
})
t.Run("registers tools with empty tool lists", func(t *testing.T) {
ts := NewToolset("test", "Test")
ts.Enabled = true
mockSrv := &mockServer{}
ts.RegisterTools(mockSrv)
assert.Len(t, mockSrv.GetTools(), 0) // No tools to register
})
}
func TestToolsetGroup_AddToolset(t *testing.T) {
t.Run("adds toolset to group", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts := NewToolset("test", "Test")
tg.AddToolset(ts)
assert.Len(t, tg.Toolsets, 1)
assert.Equal(t, ts, tg.Toolsets["test"])
// Should not be readOnly when group is not readOnly
assert.False(t, ts.readOnly)
})
t.Run("adds toolset to readOnly group", func(t *testing.T) {
tg := NewToolsetGroup(true)
ts := NewToolset("test", "Test")
tg.AddToolset(ts)
assert.Len(t, tg.Toolsets, 1)
assert.Equal(t, ts, tg.Toolsets["test"])
assert.True(t, ts.readOnly) // Should be readOnly when group is readOnly
})
t.Run("adds multiple toolsets", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
ts2 := NewToolset("test2", "Test 2")
tg.AddToolset(ts1)
tg.AddToolset(ts2)
assert.Len(t, tg.Toolsets, 2)
assert.Equal(t, ts1, tg.Toolsets["test1"])
assert.Equal(t, ts2, tg.Toolsets["test2"])
})
t.Run("overwrites toolset with same name", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test", "Test 1")
ts2 := NewToolset("test", "Test 2")
tg.AddToolset(ts1)
tg.AddToolset(ts2)
assert.Len(t, tg.Toolsets, 1)
assert.Equal(t, ts2, tg.Toolsets["test"]) // Should be the second one
})
}
func TestToolsetGroup_EnableToolset(t *testing.T) {
t.Run("enables existing toolset", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts := NewToolset("test", "Test")
tg.AddToolset(ts)
err := tg.EnableToolset("test")
assert.NoError(t, err)
assert.True(t, ts.Enabled)
})
t.Run("returns error for non-existent toolset", func(t *testing.T) {
tg := NewToolsetGroup(false)
err := tg.EnableToolset("nonexistent")
assert.Error(t, err)
assert.Contains(t, err.Error(), "does not exist")
})
t.Run("enables toolset multiple times", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts := NewToolset("test", "Test")
tg.AddToolset(ts)
err1 := tg.EnableToolset("test")
assert.NoError(t, err1)
assert.True(t, ts.Enabled)
err2 := tg.EnableToolset("test")
assert.NoError(t, err2)
assert.True(t, ts.Enabled) // Should still be enabled
})
}
func TestToolsetGroup_EnableToolsets(t *testing.T) {
t.Run("enables multiple toolsets", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
ts2 := NewToolset("test2", "Test 2")
tg.AddToolset(ts1)
tg.AddToolset(ts2)
err := tg.EnableToolsets([]string{"test1", "test2"})
assert.NoError(t, err)
assert.True(t, ts1.Enabled)
assert.True(t, ts2.Enabled)
assert.False(t, tg.everythingOn)
})
t.Run("enables all toolsets when empty array", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
ts2 := NewToolset("test2", "Test 2")
ts3 := NewToolset("test3", "Test 3")
tg.AddToolset(ts1)
tg.AddToolset(ts2)
tg.AddToolset(ts3)
err := tg.EnableToolsets([]string{})
assert.NoError(t, err)
assert.True(t, tg.everythingOn)
assert.True(t, ts1.Enabled)
assert.True(t, ts2.Enabled)
assert.True(t, ts3.Enabled)
})
t.Run("returns error when enabling non-existent toolset", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
tg.AddToolset(ts1)
err := tg.EnableToolsets([]string{"test1", "nonexistent"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "does not exist")
assert.True(t, ts1.Enabled) // First one should still be enabled
})
t.Run("enables single toolset", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts := NewToolset("test", "Test")
tg.AddToolset(ts)
err := tg.EnableToolsets([]string{"test"})
assert.NoError(t, err)
assert.True(t, ts.Enabled)
})
t.Run("handles empty toolset group", func(t *testing.T) {
tg := NewToolsetGroup(false)
err := tg.EnableToolsets([]string{})
assert.NoError(t, err)
assert.True(t, tg.everythingOn)
})
t.Run("enables all toolsets when everythingOn is true", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
ts2 := NewToolset("test2", "Test 2")
tg.AddToolset(ts1)
tg.AddToolset(ts2)
// First enable with empty array to set everythingOn
err := tg.EnableToolsets([]string{})
assert.NoError(t, err)
assert.True(t, tg.everythingOn)
assert.True(t, ts1.Enabled)
assert.True(t, ts2.Enabled)
// Reset and test the everythingOn path with non-empty array
ts1.Enabled = false
ts2.Enabled = false
tg.everythingOn = true
err = tg.EnableToolsets([]string{"test1"})
assert.NoError(t, err)
// When everythingOn is true, all toolsets should be enabled
// even though we only passed test1 in the names array
assert.True(t, ts1.Enabled)
assert.True(t, ts2.Enabled)
})
t.Run("enables all toolsets when everythingOn true with empty names",
func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
ts2 := NewToolset("test2", "Test 2")
tg.AddToolset(ts1)
tg.AddToolset(ts2)
// Set everythingOn to true
tg.everythingOn = true
ts1.Enabled = false
ts2.Enabled = false
// Call with empty array
err := tg.EnableToolsets([]string{})
assert.NoError(t, err)
assert.True(t, ts1.Enabled)
assert.True(t, ts2.Enabled)
})
}
func TestToolsetGroup_RegisterTools(t *testing.T) {
t.Run("registers tools from all enabled toolsets", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
ts2 := NewToolset("test2", "Test 2")
tool1 := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result1"), nil
})
tool2 := mcpgo.NewTool("tool2", "Tool 2", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result2"), nil
})
ts1.AddReadTools(tool1)
ts1.Enabled = true
ts2.AddReadTools(tool2)
ts2.Enabled = false // This one should not register
tg.AddToolset(ts1)
tg.AddToolset(ts2)
mockSrv := &mockServer{}
tg.RegisterTools(mockSrv)
assert.Len(t, mockSrv.GetTools(), 1) // Only tool1 should be registered
})
t.Run("registers tools from multiple enabled toolsets", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
ts2 := NewToolset("test2", "Test 2")
tool1 := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result1"), nil
})
tool2 := mcpgo.NewTool("tool2", "Tool 2", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result2"), nil
})
ts1.AddReadTools(tool1)
ts1.Enabled = true
ts2.AddReadTools(tool2)
ts2.Enabled = true
tg.AddToolset(ts1)
tg.AddToolset(ts2)
mockSrv := &mockServer{}
tg.RegisterTools(mockSrv)
assert.Len(t, mockSrv.GetTools(), 2) // Both tools should be registered
})
t.Run("registers no tools when all toolsets disabled", func(t *testing.T) {
tg := NewToolsetGroup(false)
ts1 := NewToolset("test1", "Test 1")
ts2 := NewToolset("test2", "Test 2")
tool1 := mcpgo.NewTool("tool1", "Tool 1", []mcpgo.ToolParameter{},
func(ctx context.Context,
req mcpgo.CallToolRequest) (*mcpgo.ToolResult, error) {
return mcpgo.NewToolResultText("result1"), nil
})
ts1.AddReadTools(tool1)
ts1.Enabled = false
ts2.Enabled = false
tg.AddToolset(ts1)
tg.AddToolset(ts2)
mockSrv := &mockServer{}
tg.RegisterTools(mockSrv)
assert.Len(t, mockSrv.GetTools(), 0) // No tools should be registered
})
t.Run("registers tools from empty toolset group", func(t *testing.T) {
tg := NewToolsetGroup(false)
mockSrv := &mockServer{}
tg.RegisterTools(mockSrv)
assert.Len(t, mockSrv.GetTools(), 0) // No toolsets, no tools
})
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/tokens_test.go:
--------------------------------------------------------------------------------
```go
package razorpay
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/razorpay/razorpay-go/constants"
"github.com/razorpay/razorpay-mcp-server/pkg/contextkey"
"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
"github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
)
func Test_FetchSavedPaymentMethods(t *testing.T) {
// URL patterns for mocking
createCustomerPath := fmt.Sprintf(
"/%s%s",
constants.VERSION_V1,
constants.CUSTOMER_URL,
)
fetchTokensPathFmt := fmt.Sprintf(
"/%s/customers/%%s/tokens",
constants.VERSION_V1,
)
// Sample successful customer creation/fetch response
customerResp := map[string]interface{}{
"id": "cust_1Aa00000000003",
"entity": "customer",
"name": "",
"email": "",
"contact": "9876543210",
"gstin": nil,
"notes": map[string]interface{}{},
"created_at": float64(1234567890),
}
// Sample successful tokens response
tokensResp := map[string]interface{}{
"entity": "collection",
"count": float64(2),
"items": []interface{}{
map[string]interface{}{
"id": "token_ABCDEFGH",
"entity": "token",
"token": "EhYXHrLsJdwRhM",
"bank": nil,
"wallet": nil,
"method": "card",
"card": map[string]interface{}{
"entity": "card",
"name": "Gaurav Kumar",
"last4": "1111",
"network": "Visa",
"type": "debit",
"issuer": "HDFC",
"international": false,
"emi": false,
"sub_type": "consumer",
},
"vpa": nil,
"recurring": true,
"recurring_details": map[string]interface{}{
"status": "confirmed",
"failure_reason": nil,
},
"auth_type": nil,
"mrn": nil,
"used_at": float64(1629779657),
"created_at": float64(1629779657),
"expired_at": float64(1640918400),
"dcc_enabled": false,
},
map[string]interface{}{
"id": "token_EhYXHrLsJdwRhN",
"entity": "token",
"token": "EhYXHrLsJdwRhN",
"bank": nil,
"wallet": nil,
"method": "upi",
"card": nil,
"vpa": map[string]interface{}{
"username": "gauravkumar",
"handle": "okhdfcbank",
"name": "Gaurav Kumar",
},
"recurring": true,
"recurring_details": map[string]interface{}{
"status": "confirmed",
"failure_reason": nil,
},
"auth_type": nil,
"mrn": nil,
"used_at": float64(1629779657),
"created_at": float64(1629779657),
"expired_at": float64(1640918400),
"dcc_enabled": false,
},
},
}
// Expected combined response
expectedSuccessResp := map[string]interface{}{
"customer": customerResp,
"saved_payment_methods": tokensResp,
}
// Error responses
customerCreationFailedResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "Contact number is invalid",
},
}
tokensAPIFailedResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "Customer not found",
},
}
// Customer response without ID (invalid)
invalidCustomerResp := map[string]interface{}{
"entity": "customer",
"name": "",
"email": "",
"contact": "9876543210",
"gstin": nil,
"notes": map[string]interface{}{},
"created_at": float64(1234567890),
// Missing "id" field
}
tests := []RazorpayToolTestCase{
{
Name: "successful fetch of saved cards with valid contact",
Request: map[string]interface{}{
"contact": "9876543210",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: customerResp,
},
mock.Endpoint{
Path: fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"),
Method: "GET",
Response: tokensResp,
},
)
},
ExpectError: false,
ExpectedResult: expectedSuccessResp,
},
{
Name: "successful fetch with international contact format",
Request: map[string]interface{}{
"contact": "+919876543210",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
customerRespIntl := map[string]interface{}{
"id": "cust_1Aa00000000004",
"entity": "customer",
"name": "",
"email": "",
"contact": "+919876543210",
"gstin": nil,
"notes": map[string]interface{}{},
"created_at": float64(1234567890),
}
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: customerRespIntl,
},
mock.Endpoint{
Path: fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000004"),
Method: "GET",
Response: tokensResp,
},
)
},
ExpectError: false,
ExpectedResult: map[string]interface{}{
"customer": map[string]interface{}{
"id": "cust_1Aa00000000004",
"entity": "customer",
"name": "",
"email": "",
"contact": "+919876543210",
"gstin": nil,
"notes": map[string]interface{}{},
"created_at": float64(1234567890),
},
"saved_payment_methods": tokensResp,
},
},
{
Name: "customer creation/fetch failure",
Request: map[string]interface{}{
"contact": "invalid_contact",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: customerCreationFailedResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "Failed to create/fetch customer with " +
"contact invalid_contact: Contact number is invalid",
},
{
Name: "tokens API failure after successful customer creation",
Request: map[string]interface{}{
"contact": "9876543210",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: customerResp,
},
mock.Endpoint{
Path: fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"),
Method: "GET",
Response: tokensAPIFailedResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "Failed to fetch saved payment methods for " +
"customer cust_1Aa00000000003: Customer not found",
},
{
Name: "invalid customer response - missing customer ID",
Request: map[string]interface{}{
"contact": "9876543210",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: invalidCustomerResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "Customer ID not found in response",
},
{
Name: "missing contact parameter",
Request: map[string]interface{}{
// No contact parameter
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: contact",
},
{
Name: "empty contact parameter",
Request: map[string]interface{}{
"contact": "",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: contact",
},
{
Name: "null contact parameter",
Request: map[string]interface{}{
"contact": nil,
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: contact",
},
{
Name: "successful fetch with empty tokens list",
Request: map[string]interface{}{
"contact": "9876543210",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
emptyTokensResp := map[string]interface{}{
"entity": "collection",
"count": float64(0),
"items": []interface{}{},
}
return mock.NewHTTPClient(
mock.Endpoint{
Path: createCustomerPath,
Method: "POST",
Response: customerResp,
},
mock.Endpoint{
Path: fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"),
Method: "GET",
Response: emptyTokensResp,
},
)
},
ExpectError: false,
ExpectedResult: map[string]interface{}{
"customer": customerResp,
"saved_payment_methods": map[string]interface{}{
"entity": "collection",
"count": float64(0),
"items": []interface{}{},
},
},
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchSavedPaymentMethods, "Saved Cards")
})
}
}
// Test_FetchSavedPaymentMethods_ClientContextScenarios tests scenarios
// related to client context handling for 100% code coverage
func Test_FetchSavedPaymentMethods_ClientContextScenarios(t *testing.T) {
obs := CreateTestObservability()
t.Run("no client in context and default is nil", func(t *testing.T) {
// Create tool with nil client
tool := FetchSavedPaymentMethods(obs, nil)
// Create context without client
ctx := context.Background()
request := mcpgo.CallToolRequest{
Arguments: map[string]interface{}{
"contact": "9876543210",
},
}
result, err := tool.GetHandler()(ctx, request)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
if result.Text == "" {
t.Fatal("Expected error message in result")
}
expectedErrMsg := "no client found in context"
if !strings.Contains(result.Text, expectedErrMsg) {
t.Errorf(
"Expected error message to contain '%s', got '%s'",
expectedErrMsg,
result.Text,
)
}
})
t.Run("invalid client type in context", func(t *testing.T) {
// Create tool with nil client
tool := FetchSavedPaymentMethods(obs, nil)
// Create context with invalid client type
ctx := contextkey.WithClient(context.Background(), "invalid_client_type")
request := mcpgo.CallToolRequest{
Arguments: map[string]interface{}{
"contact": "9876543210",
},
}
result, err := tool.GetHandler()(ctx, request)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
if result.Text == "" {
t.Fatal("Expected error message in result")
}
expectedErrMsg := "invalid client type in context"
if !strings.Contains(result.Text, expectedErrMsg) {
t.Errorf(
"Expected error message to contain '%s', got '%s'",
expectedErrMsg,
result.Text,
)
}
})
}
func Test_RevokeToken(t *testing.T) {
// URL patterns for mocking
revokeTokenPathFmt := fmt.Sprintf(
"/%s/customers/%%s/tokens/%%s/cancel",
constants.VERSION_V1,
)
// Sample successful token revocation response
successResp := map[string]interface{}{
"deleted": true,
}
// Error responses
tokenNotFoundResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "Token not found",
},
}
customerNotFoundResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "Customer not found",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful token revocation with valid parameters",
Request: map[string]interface{}{
"customer_id": "cust_1Aa00000000003",
"token_id": "token_ABCDEFGH",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
revokeTokenPathFmt,
"cust_1Aa00000000003",
"token_ABCDEFGH",
),
Method: "PUT",
Response: successResp,
},
)
},
ExpectError: false,
ExpectedResult: successResp,
},
{
Name: "token not found error",
Request: map[string]interface{}{
"customer_id": "cust_1Aa00000000003",
"token_id": "token_nonexistent",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
revokeTokenPathFmt,
"cust_1Aa00000000003",
"token_nonexistent",
),
Method: "PUT",
Response: tokenNotFoundResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "Failed to revoke token token_nonexistent for " +
"customer cust_1Aa00000000003: Token not found",
},
{
Name: "customer not found error",
Request: map[string]interface{}{
"customer_id": "cust_nonexistent",
"token_id": "token_ABCDEFGH",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
revokeTokenPathFmt,
"cust_nonexistent",
"token_ABCDEFGH",
),
Method: "PUT",
Response: customerNotFoundResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "Failed to revoke token token_ABCDEFGH for " +
"customer cust_nonexistent: Customer not found",
},
{
Name: "missing customer_id parameter",
Request: map[string]interface{}{
"token_id": "token_ABCDEFGH",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: customer_id",
},
{
Name: "missing token_id parameter",
Request: map[string]interface{}{
"customer_id": "cust_1Aa00000000003",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: token_id",
},
{
Name: "empty customer_id parameter",
Request: map[string]interface{}{
"customer_id": "",
"token_id": "token_ABCDEFGH",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: customer_id",
},
{
Name: "empty token_id parameter",
Request: map[string]interface{}{
"customer_id": "cust_1Aa00000000003",
"token_id": "",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: token_id",
},
{
Name: "null customer_id parameter",
Request: map[string]interface{}{
"customer_id": nil,
"token_id": "token_ABCDEFGH",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: customer_id",
},
{
Name: "null token_id parameter",
Request: map[string]interface{}{
"customer_id": "cust_1Aa00000000003",
"token_id": nil,
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: token_id",
},
{
Name: "both parameters missing",
Request: map[string]interface{}{
// No parameters
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: customer_id",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, RevokeToken, "Revoke Token")
})
}
}
// Test_RevokeToken_ClientContextScenarios tests scenarios
// related to client context handling for 100% code coverage
func Test_RevokeToken_ClientContextScenarios(t *testing.T) {
obs := CreateTestObservability()
t.Run("no client in context and default is nil", func(t *testing.T) {
// Create tool with nil client
tool := RevokeToken(obs, nil)
// Create context without client
ctx := context.Background()
request := mcpgo.CallToolRequest{
Arguments: map[string]interface{}{
"customer_id": "cust_1Aa00000000003",
"token_id": "token_ABCDEFGH",
},
}
result, err := tool.GetHandler()(ctx, request)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
if result.Text == "" {
t.Fatal("Expected error message in result")
}
expectedErrMsg := "no client found in context"
if !strings.Contains(result.Text, expectedErrMsg) {
t.Errorf(
"Expected error message to contain '%s', got '%s'",
expectedErrMsg,
result.Text,
)
}
})
t.Run("invalid client type in context", func(t *testing.T) {
// Create tool with nil client
tool := RevokeToken(obs, nil)
// Create context with invalid client type
ctx := contextkey.WithClient(context.Background(), "invalid_client_type")
request := mcpgo.CallToolRequest{
Arguments: map[string]interface{}{
"customer_id": "cust_1Aa00000000003",
"token_id": "token_ABCDEFGH",
},
}
result, err := tool.GetHandler()(ctx, request)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result == nil {
t.Fatal("Expected result, got nil")
}
if result.Text == "" {
t.Fatal("Expected error message in result")
}
expectedErrMsg := "invalid client type in context"
if !strings.Contains(result.Text, expectedErrMsg) {
t.Errorf(
"Expected error message to contain '%s', got '%s'",
expectedErrMsg,
result.Text,
)
}
})
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/payment_links_test.go:
--------------------------------------------------------------------------------
```go
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_CreatePaymentLink(t *testing.T) {
createPaymentLinkPath := fmt.Sprintf(
"/%s%s",
constants.VERSION_V1,
constants.PaymentLink_URL,
)
successfulPaymentLinkResp := map[string]interface{}{
"id": "plink_ExjpAUN3gVHrPJ",
"amount": float64(50000),
"currency": "INR",
"description": "Test payment",
"status": "created",
"short_url": "https://rzp.io/i/nxrHnLJ",
}
paymentLinkWithoutDescResp := map[string]interface{}{
"id": "plink_ExjpAUN3gVHrPJ",
"amount": float64(50000),
"currency": "INR",
"status": "created",
"short_url": "https://rzp.io/i/nxrHnLJ",
}
invalidCurrencyErrorResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "API error: Invalid currency",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful payment link creation",
Request: map[string]interface{}{
"amount": float64(50000),
"currency": "INR",
"description": "Test payment",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createPaymentLinkPath,
Method: "POST",
Response: successfulPaymentLinkResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulPaymentLinkResp,
},
{
Name: "payment link without description",
Request: map[string]interface{}{
"amount": float64(50000),
"currency": "INR",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createPaymentLinkPath,
Method: "POST",
Response: paymentLinkWithoutDescResp,
},
)
},
ExpectError: false,
ExpectedResult: paymentLinkWithoutDescResp,
},
{
Name: "missing amount parameter",
Request: map[string]interface{}{
"currency": "INR",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: amount",
},
{
Name: "missing currency parameter",
Request: map[string]interface{}{
"amount": float64(50000),
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: currency",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
// Missing both amount and currency (required parameters)
"description": 12345, // Wrong type for description
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "Validation errors:\n- " +
"missing required parameter: amount\n- " +
"missing required parameter: currency\n- " +
"invalid parameter type: description",
},
{
Name: "payment link creation fails",
Request: map[string]interface{}{
"amount": float64(50000),
"currency": "XYZ", // Invalid currency
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createPaymentLinkPath,
Method: "POST",
Response: invalidCurrencyErrorResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "creating payment link failed: API error: Invalid currency",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, CreatePaymentLink, "Payment Link")
})
}
}
func Test_FetchPaymentLink(t *testing.T) {
fetchPaymentLinkPathFmt := fmt.Sprintf(
"/%s%s/%%s",
constants.VERSION_V1,
constants.PaymentLink_URL,
)
// Define common response maps to be reused
paymentLinkResp := map[string]interface{}{
"id": "plink_ExjpAUN3gVHrPJ",
"amount": float64(50000),
"currency": "INR",
"description": "Test payment",
"status": "paid",
"short_url": "https://rzp.io/i/nxrHnLJ",
}
paymentLinkNotFoundResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "payment link not found",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful payment link fetch",
Request: map[string]interface{}{
"payment_link_id": "plink_ExjpAUN3gVHrPJ",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(fetchPaymentLinkPathFmt, "plink_ExjpAUN3gVHrPJ"),
Method: "GET",
Response: paymentLinkResp,
},
)
},
ExpectError: false,
ExpectedResult: paymentLinkResp,
},
{
Name: "payment link not found",
Request: map[string]interface{}{
"payment_link_id": "plink_invalid",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(fetchPaymentLinkPathFmt, "plink_invalid"),
Method: "GET",
Response: paymentLinkNotFoundResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching payment link failed: payment link not found",
},
{
Name: "missing payment_link_id parameter",
Request: map[string]interface{}{},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: payment_link_id",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
// Missing payment_link_id parameter
"non_existent_param": 12345, // Additional parameter that doesn't exist
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: payment_link_id",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchPaymentLink, "Payment Link")
})
}
}
func Test_CreateUpiPaymentLink(t *testing.T) {
createPaymentLinkPath := fmt.Sprintf(
"/%s%s",
constants.VERSION_V1,
constants.PaymentLink_URL,
)
upiPaymentLinkWithAllParamsResp := map[string]interface{}{
"id": "plink_UpiAllParamsExjpAUN3gVHrPJ",
"amount": float64(50000),
"currency": "INR",
"description": "Test UPI payment with all params",
"reference_id": "REF12345",
"accept_partial": true,
"expire_by": float64(1718196584),
"reminder_enable": true,
"status": "created",
"short_url": "https://rzp.io/i/upiAllParams123",
"upi_link": true,
"customer": map[string]interface{}{
"name": "Test Customer",
"email": "[email protected]",
"contact": "+919876543210",
},
"notes": map[string]interface{}{
"policy_name": "Test Policy",
"user_id": "usr_123",
},
}
errorResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "API error: Something went wrong",
},
}
tests := []RazorpayToolTestCase{
{
Name: "UPI payment link with all parameters",
Request: map[string]interface{}{
"amount": float64(50000),
"currency": "INR",
"description": "Test UPI payment with all params",
"reference_id": "REF12345",
"accept_partial": true,
"first_min_partial_amount": float64(10000),
"expire_by": float64(1718196584),
"customer_name": "Test Customer",
"customer_email": "[email protected]",
"customer_contact": "+919876543210",
"notify_sms": true,
"notify_email": true,
"reminder_enable": true,
"notes": map[string]interface{}{
"policy_name": "Test Policy",
"user_id": "usr_123",
},
"callback_url": "https://example.com/callback",
"callback_method": "get",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createPaymentLinkPath,
Method: "POST",
Response: upiPaymentLinkWithAllParamsResp,
},
)
},
ExpectError: false,
ExpectedResult: upiPaymentLinkWithAllParamsResp,
},
{
Name: "missing amount parameter",
Request: map[string]interface{}{},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: amount",
},
{
Name: "UPI payment link creation fails",
Request: map[string]interface{}{
"amount": float64(50000),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createPaymentLinkPath,
Method: "POST",
Response: errorResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "missing required parameter: currency",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, CreateUpiPaymentLink, "UPI Payment Link")
})
}
}
func Test_ResendPaymentLinkNotification(t *testing.T) {
notifyPaymentLinkPathFmt := fmt.Sprintf(
"/%s%s/%%s/notify_by/%%s",
constants.VERSION_V1,
constants.PaymentLink_URL,
)
successResponse := map[string]interface{}{
"success": true,
}
invalidMediumErrorResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "not a valid notification medium",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful SMS notification",
Request: map[string]interface{}{
"payment_link_id": "plink_ExjpAUN3gVHrPJ",
"medium": "sms",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
notifyPaymentLinkPathFmt,
"plink_ExjpAUN3gVHrPJ",
"sms",
),
Method: "POST",
Response: successResponse,
},
)
},
ExpectError: false,
ExpectedResult: successResponse,
},
{
Name: "missing payment_link_id parameter",
Request: map[string]interface{}{
"medium": "sms",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: payment_link_id",
},
{
Name: "missing medium parameter",
Request: map[string]interface{}{
"payment_link_id": "plink_ExjpAUN3gVHrPJ",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: medium",
},
{
Name: "API error response",
Request: map[string]interface{}{
"payment_link_id": "plink_Invalid",
"medium": "sms", // Using valid medium so it passes validation
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
notifyPaymentLinkPathFmt,
"plink_Invalid",
"sms",
),
Method: "POST",
Response: invalidMediumErrorResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "sending notification failed: " +
"not a valid notification medium",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
toolFunc := ResendPaymentLinkNotification
runToolTest(t, tc, toolFunc, "Payment Link Notification")
})
}
}
func Test_UpdatePaymentLink(t *testing.T) {
updatePaymentLinkPathFmt := fmt.Sprintf(
"/%s%s/%%s",
constants.VERSION_V1,
constants.PaymentLink_URL,
)
updatedPaymentLinkResp := map[string]interface{}{
"id": "plink_FL5HCrWEO112OW",
"amount": float64(1000),
"currency": "INR",
"status": "created",
"reference_id": "TS35",
"expire_by": float64(1612092283),
"reminder_enable": false,
"notes": []interface{}{
map[string]interface{}{
"key": "policy_name",
"value": "Jeevan Saral",
},
},
}
invalidStateResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "update can only be made in created or partially paid state",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful update with multiple fields",
Request: map[string]interface{}{
"payment_link_id": "plink_FL5HCrWEO112OW",
"reference_id": "TS35",
"expire_by": float64(1612092283),
"reminder_enable": false,
"accept_partial": true,
"notes": map[string]interface{}{
"policy_name": "Jeevan Saral",
},
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
updatePaymentLinkPathFmt,
"plink_FL5HCrWEO112OW",
),
Method: "PATCH",
Response: updatedPaymentLinkResp,
},
)
},
ExpectError: false,
ExpectedResult: updatedPaymentLinkResp,
},
{
Name: "successful update with single field",
Request: map[string]interface{}{
"payment_link_id": "plink_FL5HCrWEO112OW",
"reference_id": "TS35",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
updatePaymentLinkPathFmt,
"plink_FL5HCrWEO112OW",
),
Method: "PATCH",
Response: updatedPaymentLinkResp,
},
)
},
ExpectError: false,
ExpectedResult: updatedPaymentLinkResp,
},
{
Name: "missing payment_link_id parameter",
Request: map[string]interface{}{
"reference_id": "TS35",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: payment_link_id",
},
{
Name: "no update fields provided",
Request: map[string]interface{}{
"payment_link_id": "plink_FL5HCrWEO112OW",
},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "at least one field to update must be provided",
},
{
Name: "payment link in invalid state",
Request: map[string]interface{}{
"payment_link_id": "plink_Paid",
"reference_id": "TS35",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
updatePaymentLinkPathFmt,
"plink_Paid",
),
Method: "PATCH",
Response: invalidStateResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "updating payment link failed: update can only be made in " +
"created or partially paid state",
},
{
Name: "update with explicit false value",
Request: map[string]interface{}{
"payment_link_id": "plink_FL5HCrWEO112OW",
"reminder_enable": false, // Explicitly set to false
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
updatePaymentLinkPathFmt,
"plink_FL5HCrWEO112OW",
),
Method: "PATCH",
Response: updatedPaymentLinkResp,
},
)
},
ExpectError: false,
ExpectedResult: updatedPaymentLinkResp,
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
toolFunc := UpdatePaymentLink
runToolTest(t, tc, toolFunc, "Payment Link Update")
})
}
}
func Test_FetchAllPaymentLinks(t *testing.T) {
fetchAllPaymentLinksPath := fmt.Sprintf(
"/%s%s",
constants.VERSION_V1,
constants.PaymentLink_URL,
)
allPaymentLinksResp := map[string]interface{}{
"payment_links": []interface{}{
map[string]interface{}{
"id": "plink_KBnb7I424Rc1R9",
"amount": float64(10000),
"currency": "INR",
"status": "paid",
"description": "Grocery",
"reference_id": "111",
"short_url": "https://rzp.io/i/alaBxs0i",
"upi_link": false,
},
map[string]interface{}{
"id": "plink_JP6yOUDCuHgcrl",
"amount": float64(10000),
"currency": "INR",
"status": "paid",
"description": "Online Tutoring - 1 Month",
"reference_id": "11212",
"short_url": "https://rzp.io/i/0ioYuawFu",
"upi_link": false,
},
},
}
errorResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "The api key/secret provided is invalid",
},
}
tests := []RazorpayToolTestCase{
{
Name: "fetch all payment links",
Request: map[string]interface{}{},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllPaymentLinksPath,
Method: "GET",
Response: allPaymentLinksResp,
},
)
},
ExpectError: false,
ExpectedResult: allPaymentLinksResp,
},
{
Name: "api error",
Request: map[string]interface{}{},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllPaymentLinksPath,
Method: "GET",
Response: errorResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching payment links failed: The api key/secret provided is invalid", // nolint:lll
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
toolFunc := FetchAllPaymentLinks
runToolTest(t, tc, toolFunc, "Payment Links")
})
}
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/payment_links.go:
--------------------------------------------------------------------------------
```go
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"
)
// CreatePaymentLink returns a tool that creates payment links in Razorpay
func CreatePaymentLink(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithNumber(
"amount",
mcpgo.Description("Amount to be paid using the link in smallest "+
"currency unit(e.g., ₹300, use 30000)"),
mcpgo.Required(),
mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
),
mcpgo.WithString(
"currency",
mcpgo.Description("Three-letter ISO code for the currency (e.g., INR)"),
mcpgo.Required(),
),
mcpgo.WithString(
"description",
mcpgo.Description("A brief description of the Payment Link explaining the intent of the payment."), // nolint:lll
),
mcpgo.WithBoolean(
"accept_partial",
mcpgo.Description("Indicates whether customers can make partial payments using the Payment Link. Default: false"), // nolint:lll
),
mcpgo.WithNumber(
"first_min_partial_amount",
mcpgo.Description("Minimum amount that must be paid by the customer as the first partial payment. Default value is 100."), // nolint:lll
),
mcpgo.WithNumber(
"expire_by",
mcpgo.Description("Timestamp, in Unix, when the Payment Link will expire. By default, a Payment Link will be valid for six months."), // nolint:lll
),
mcpgo.WithString(
"reference_id",
mcpgo.Description("Reference number tagged to a Payment Link. Must be unique for each Payment Link. Max 40 characters."), // nolint:lll
),
mcpgo.WithString(
"customer_name",
mcpgo.Description("Name of the customer."),
),
mcpgo.WithString(
"customer_email",
mcpgo.Description("Email address of the customer."),
),
mcpgo.WithString(
"customer_contact",
mcpgo.Description("Contact number of the customer."),
),
mcpgo.WithBoolean(
"notify_sms",
mcpgo.Description("Send SMS notifications for the Payment Link."),
),
mcpgo.WithBoolean(
"notify_email",
mcpgo.Description("Send email notifications for the Payment Link."),
),
mcpgo.WithBoolean(
"reminder_enable",
mcpgo.Description("Enable payment reminders for the Payment Link."),
),
mcpgo.WithObject(
"notes",
mcpgo.Description("Key-value pairs that can be used to store additional information. Maximum 15 pairs, each value limited to 256 characters."), // nolint:lll
),
mcpgo.WithString(
"callback_url",
mcpgo.Description("If specified, adds a redirect URL to the Payment Link. Customer will be redirected here after payment."), // nolint:lll
),
mcpgo.WithString(
"callback_method",
mcpgo.Description("HTTP method for callback redirection. "+
"Must be 'get' if callback_url is set."),
),
}
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
}
// Create a parameters map to collect validated parameters
plCreateReq := make(map[string]interface{})
customer := make(map[string]interface{})
notify := make(map[string]interface{})
// Validate all parameters with fluent validator
validator := NewValidator(&r).
ValidateAndAddRequiredInt(plCreateReq, "amount").
ValidateAndAddRequiredString(plCreateReq, "currency").
ValidateAndAddOptionalString(plCreateReq, "description").
ValidateAndAddOptionalBool(plCreateReq, "accept_partial").
ValidateAndAddOptionalInt(plCreateReq, "first_min_partial_amount").
ValidateAndAddOptionalInt(plCreateReq, "expire_by").
ValidateAndAddOptionalString(plCreateReq, "reference_id").
ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact").
ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email").
ValidateAndAddOptionalBool(plCreateReq, "reminder_enable").
ValidateAndAddOptionalMap(plCreateReq, "notes").
ValidateAndAddOptionalString(plCreateReq, "callback_url").
ValidateAndAddOptionalString(plCreateReq, "callback_method")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Handle customer details
if len(customer) > 0 {
plCreateReq["customer"] = customer
}
// Handle notification settings
if len(notify) > 0 {
plCreateReq["notify"] = notify
}
// Create the payment link
paymentLink, err := client.PaymentLink.Create(plCreateReq, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("creating payment link failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(paymentLink)
}
return mcpgo.NewTool(
"create_payment_link",
"Create a new standard payment link in Razorpay with a specified amount",
parameters,
handler,
)
}
// CreateUpiPaymentLink returns a tool that creates payment links in Razorpay
func CreateUpiPaymentLink(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithNumber(
"amount",
mcpgo.Description("Amount to be paid using the link in smallest currency unit(e.g., ₹300, use 30000), Only accepted currency is INR"), // nolint:lll
mcpgo.Required(),
mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
),
mcpgo.WithString(
"currency",
mcpgo.Description("Three-letter ISO code for the currency (e.g., INR). UPI links are only supported in INR"), // nolint:lll
mcpgo.Required(),
),
mcpgo.WithString(
"description",
mcpgo.Description("A brief description of the Payment Link explaining the intent of the payment."), // nolint:lll
),
mcpgo.WithBoolean(
"accept_partial",
mcpgo.Description("Indicates whether customers can make partial payments using the Payment Link. Default: false"), // nolint:lll
),
mcpgo.WithNumber(
"first_min_partial_amount",
mcpgo.Description("Minimum amount that must be paid by the customer as the first partial payment. Default value is 100."), // nolint:lll
),
mcpgo.WithNumber(
"expire_by",
mcpgo.Description("Timestamp, in Unix, when the Payment Link will expire. By default, a Payment Link will be valid for six months."), // nolint:lll
),
mcpgo.WithString(
"reference_id",
mcpgo.Description("Reference number tagged to a Payment Link. Must be unique for each Payment Link. Max 40 characters."), // nolint:lll
),
mcpgo.WithString(
"customer_name",
mcpgo.Description("Name of the customer."),
),
mcpgo.WithString(
"customer_email",
mcpgo.Description("Email address of the customer."),
),
mcpgo.WithString(
"customer_contact",
mcpgo.Description("Contact number of the customer."),
),
mcpgo.WithBoolean(
"notify_sms",
mcpgo.Description("Send SMS notifications for the Payment Link."),
),
mcpgo.WithBoolean(
"notify_email",
mcpgo.Description("Send email notifications for the Payment Link."),
),
mcpgo.WithBoolean(
"reminder_enable",
mcpgo.Description("Enable payment reminders for the Payment Link."),
),
mcpgo.WithObject(
"notes",
mcpgo.Description("Key-value pairs that can be used to store additional information. Maximum 15 pairs, each value limited to 256 characters."), // nolint:lll
),
mcpgo.WithString(
"callback_url",
mcpgo.Description("If specified, adds a redirect URL to the Payment Link. Customer will be redirected here after payment."), // nolint:lll
),
mcpgo.WithString(
"callback_method",
mcpgo.Description("HTTP method for callback redirection. "+
"Must be 'get' if callback_url is set."),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Create a parameters map to collect validated parameters
upiPlCreateReq := make(map[string]interface{})
customer := make(map[string]interface{})
notify := make(map[string]interface{})
// Validate all parameters with fluent validator
validator := NewValidator(&r).
ValidateAndAddRequiredInt(upiPlCreateReq, "amount").
ValidateAndAddRequiredString(upiPlCreateReq, "currency").
ValidateAndAddOptionalString(upiPlCreateReq, "description").
ValidateAndAddOptionalBool(upiPlCreateReq, "accept_partial").
ValidateAndAddOptionalInt(upiPlCreateReq, "first_min_partial_amount").
ValidateAndAddOptionalInt(upiPlCreateReq, "expire_by").
ValidateAndAddOptionalString(upiPlCreateReq, "reference_id").
ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact").
ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email").
ValidateAndAddOptionalBool(upiPlCreateReq, "reminder_enable").
ValidateAndAddOptionalMap(upiPlCreateReq, "notes").
ValidateAndAddOptionalString(upiPlCreateReq, "callback_url").
ValidateAndAddOptionalString(upiPlCreateReq, "callback_method")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Add the required UPI payment link parameters
upiPlCreateReq["upi_link"] = "true"
// Handle customer details
if len(customer) > 0 {
upiPlCreateReq["customer"] = customer
}
// Handle notification settings
if len(notify) > 0 {
upiPlCreateReq["notify"] = notify
}
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
// Create the payment link
paymentLink, err := client.PaymentLink.Create(upiPlCreateReq, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("upi pl create failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(paymentLink)
}
return mcpgo.NewTool(
"payment_link_upi_create",
"Create a new UPI payment link in Razorpay with a specified amount and additional options.", // nolint:lll
parameters,
handler,
)
}
// FetchPaymentLink returns a tool that fetches payment link details using
// payment_link_id
func FetchPaymentLink(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_link_id",
mcpgo.Description("ID of the payment link to be fetched"+
"(ID should have a plink_ prefix)."),
mcpgo.Required(),
),
}
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
}
fields := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(fields, "payment_link_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentLinkId := fields["payment_link_id"].(string)
paymentLink, err := client.PaymentLink.Fetch(paymentLinkId, nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching payment link failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(paymentLink)
}
return mcpgo.NewTool(
"fetch_payment_link",
"Fetch payment link details using it's ID. "+
"Response contains the basic details like amount, status etc. "+
"The link could be of any type(standard or UPI)",
parameters,
handler,
)
}
// ResendPaymentLinkNotification returns a tool that sends/resends notifications
// for a payment link via email or SMS
func ResendPaymentLinkNotification(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_link_id",
mcpgo.Description("ID of the payment link for which to send notification "+
"(ID should have a plink_ prefix)."), // nolint:lll
mcpgo.Required(),
),
mcpgo.WithString(
"medium",
mcpgo.Description("Medium through which to send the notification. "+
"Must be either 'sms' or 'email'."), // nolint:lll
mcpgo.Required(),
mcpgo.Enum("sms", "email"),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
fields := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(fields, "payment_link_id").
ValidateAndAddRequiredString(fields, "medium")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentLinkId := fields["payment_link_id"].(string)
medium := fields["medium"].(string)
// Call the SDK function
response, err := client.PaymentLink.NotifyBy(paymentLinkId, medium, nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("sending notification failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(response)
}
return mcpgo.NewTool(
"payment_link_notify",
"Send or resend notification for a payment link via SMS or email.", // nolint:lll
parameters,
handler,
)
}
// UpdatePaymentLink returns a tool that updates an existing payment link
func UpdatePaymentLink(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_link_id",
mcpgo.Description("ID of the payment link to update "+
"(ID should have a plink_ prefix)."),
mcpgo.Required(),
),
mcpgo.WithString(
"reference_id",
mcpgo.Description("Adds a unique reference number to the payment link."),
),
mcpgo.WithNumber(
"expire_by",
mcpgo.Description("Timestamp, in Unix format, when the payment link "+
"should expire."),
),
mcpgo.WithBoolean(
"reminder_enable",
mcpgo.Description("Enable or disable reminders for the payment link."),
),
mcpgo.WithBoolean(
"accept_partial",
mcpgo.Description("Allow customers to make partial payments. "+
"Not allowed with UPI payment links."),
),
mcpgo.WithObject(
"notes",
mcpgo.Description("Key-value pairs for additional information. "+
"Maximum 15 pairs, each value limited to 256 characters."),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
plUpdateReq := make(map[string]interface{})
otherFields := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(otherFields, "payment_link_id").
ValidateAndAddOptionalString(plUpdateReq, "reference_id").
ValidateAndAddOptionalInt(plUpdateReq, "expire_by").
ValidateAndAddOptionalBool(plUpdateReq, "reminder_enable").
ValidateAndAddOptionalBool(plUpdateReq, "accept_partial").
ValidateAndAddOptionalMap(plUpdateReq, "notes")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentLinkId := otherFields["payment_link_id"].(string)
// Ensure we have at least one field to update
if len(plUpdateReq) == 0 {
return mcpgo.NewToolResultError(
"at least one field to update must be provided"), nil
}
// Call the SDK function
paymentLink, err := client.PaymentLink.Update(paymentLinkId, plUpdateReq, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("updating payment link failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(paymentLink)
}
return mcpgo.NewTool(
"update_payment_link",
"Update any existing standard or UPI payment link with new details such as reference ID, "+ // nolint:lll
"expiry date, or notes.",
parameters,
handler,
)
}
// FetchAllPaymentLinks returns a tool that fetches all payment links
// with optional filtering
func FetchAllPaymentLinks(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_id",
mcpgo.Description("Optional: Filter by payment ID associated with payment links"), // nolint:lll
),
mcpgo.WithString(
"reference_id",
mcpgo.Description("Optional: Filter by reference ID used when creating payment links"), // nolint:lll
),
mcpgo.WithNumber(
"upi_link",
mcpgo.Description("Optional: Filter only upi links. "+
"Value should be 1 if you want only upi links, 0 for only standard links"+
"If not provided, all types of links will be returned"),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
plListReq := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddOptionalString(plListReq, "payment_id").
ValidateAndAddOptionalString(plListReq, "reference_id").
ValidateAndAddOptionalInt(plListReq, "upi_link")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Call the API directly using the Request object
response, err := client.PaymentLink.All(plListReq, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching payment links failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(response)
}
return mcpgo.NewTool(
"fetch_all_payment_links",
"Fetch all payment links with optional filtering by payment ID or reference ID."+ // nolint:lll
"You can specify the upi_link parameter to filter by link type.",
parameters,
handler,
)
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/refunds_test.go:
--------------------------------------------------------------------------------
```go
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_CreateRefund(t *testing.T) {
createRefundPathFmt := fmt.Sprintf(
"/%s%s/%%s/refund",
constants.VERSION_V1,
constants.PAYMENT_URL,
)
// Define test responses
successfulRefundResp := map[string]interface{}{
"id": "rfnd_FP8QHiV938haTz",
"entity": "refund",
"amount": float64(500100),
"currency": "INR",
"payment_id": "pay_29QQoUBi66xm2f",
"notes": map[string]interface{}{},
"receipt": "Receipt No. 31",
"acquirer_data": map[string]interface{}{"arn": nil},
"created_at": float64(1597078866),
"batch_id": nil,
"status": "processed",
"speed_processed": "normal",
"speed_requested": "normal",
}
errorResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "Razorpay API error: Bad request",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful full refund",
Request: map[string]interface{}{
"payment_id": "pay_29QQoUBi66xm2f",
"amount": float64(500100),
"receipt": "Receipt No. 31",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
Method: "POST",
Response: successfulRefundResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulRefundResp,
},
{
Name: "refund with speed parameter",
Request: map[string]interface{}{
"payment_id": "pay_29QQoUBi66xm2f",
"amount": float64(500100),
"speed": "optimum",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
speedRefundResp := map[string]interface{}{
"id": "rfnd_HzAbPEkKtRq48V",
"entity": "refund",
"amount": float64(500100),
"payment_id": "pay_29QQoUBi66xm2f",
"status": "processed",
"speed_processed": "instant",
"speed_requested": "optimum",
}
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
Method: "POST",
Response: speedRefundResp,
},
)
},
ExpectError: false,
ExpectedResult: map[string]interface{}{
"id": "rfnd_HzAbPEkKtRq48V",
"entity": "refund",
"amount": float64(500100),
"payment_id": "pay_29QQoUBi66xm2f",
"status": "processed",
"speed_processed": "instant",
"speed_requested": "optimum",
},
},
{
Name: "refund API server error",
Request: map[string]interface{}{
"payment_id": "pay_29QQoUBi66xm2f",
"amount": float64(500100),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
Method: "POST",
Response: errorResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "creating refund failed: Razorpay API error: Bad request",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
// Missing payment_id parameter
"amount": "not-a-number", // Wrong type for amount
"speed": 12345, // Wrong type for speed
"notes": "not-an-object", // Wrong type for notes
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "Validation errors:\n- " +
"missing required parameter: payment_id\n- " +
"invalid parameter type: amount\n- " +
"invalid parameter type: speed\n- " +
"invalid parameter type: notes",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, CreateRefund, "Refund")
})
}
}
func Test_FetchRefund(t *testing.T) {
fetchRefundPathFmt := fmt.Sprintf(
"/%s%s/%%s",
constants.VERSION_V1,
constants.REFUND_URL,
)
// Define test response for successful refund fetch
successfulRefundResp := map[string]interface{}{
"id": "rfnd_DfjjhJC6eDvUAi",
"entity": "refund",
"amount": float64(6000),
"currency": "INR",
"payment_id": "pay_EpkFDYRirena0f",
"notes": map[string]interface{}{
"comment": "Issuing an instant refund",
},
"receipt": nil,
"acquirer_data": map[string]interface{}{
"arn": "10000000000000",
},
"created_at": float64(1589521675),
"batch_id": nil,
"status": "processed",
"speed_processed": "optimum",
"speed_requested": "optimum",
}
// Define error responses
notFoundResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "The id provided does not exist",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful refund fetch",
Request: map[string]interface{}{
"refund_id": "rfnd_DfjjhJC6eDvUAi",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(fetchRefundPathFmt, "rfnd_DfjjhJC6eDvUAi"),
Method: "GET",
Response: successfulRefundResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulRefundResp,
},
{
Name: "refund id not found",
Request: map[string]interface{}{
"refund_id": "rfnd_nonexistent",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(fetchRefundPathFmt, "rfnd_nonexistent"),
Method: "GET",
Response: notFoundResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching refund failed: The id provided does not exist",
},
{
Name: "missing refund_id parameter",
Request: map[string]interface{}{},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "missing required parameter: refund_id",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
// Missing refund_id parameter
"non_existent_param": 12345, // Additional parameter that doesn't exist
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "missing required parameter: refund_id",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchRefund, "Refund")
})
}
}
func Test_UpdateRefund(t *testing.T) {
updateRefundPathFmt := fmt.Sprintf(
"/%s%s/%%s",
constants.VERSION_V1,
constants.REFUND_URL,
)
// Define test response for successful refund update
successfulUpdateResp := map[string]interface{}{
"id": "rfnd_DfjjhJC6eDvUAi",
"entity": "refund",
"amount": float64(300100),
"currency": "INR",
"payment_id": "pay_FIKOnlyii5QGNx",
"notes": map[string]interface{}{
"notes_key_1": "Beam me up Scotty.",
"notes_key_2": "Engage",
},
"receipt": nil,
"acquirer_data": map[string]interface{}{"arn": "10000000000000"},
"created_at": float64(1597078124),
"batch_id": nil,
"status": "processed",
"speed_processed": "normal",
"speed_requested": "optimum",
}
// Define error responses
notFoundResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "The id provided does not exist",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful refund update",
Request: map[string]interface{}{
"refund_id": "rfnd_DfjjhJC6eDvUAi",
"notes": map[string]interface{}{
"notes_key_1": "Beam me up Scotty.",
"notes_key_2": "Engage",
},
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(updateRefundPathFmt, "rfnd_DfjjhJC6eDvUAi"),
Method: "PATCH",
Response: successfulUpdateResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulUpdateResp,
},
{
Name: "refund id not found",
Request: map[string]interface{}{
"refund_id": "rfnd_nonexistent",
"notes": map[string]interface{}{
"note_key": "Test note",
},
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(updateRefundPathFmt, "rfnd_nonexistent"),
Method: "PATCH",
Response: notFoundResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "updating refund failed: The id provided does not exist",
},
{
Name: "missing refund_id parameter",
Request: map[string]interface{}{},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "missing required parameter: refund_id",
},
{
Name: "missing notes parameter",
Request: map[string]interface{}{
"refund_id": "rfnd_DfjjhJC6eDvUAi",
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "missing required parameter: notes",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
// Missing both refund_id and notes parameters
"non_existent_param": 12345, // Additional parameter that doesn't exist
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "Validation errors:\n- " +
"missing required parameter: refund_id\n- " +
"missing required parameter: notes",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, UpdateRefund, "Refund")
})
}
}
func Test_FetchMultipleRefundsForPayment(t *testing.T) {
fetchMultipleRefundsPathFmt := fmt.Sprintf(
"/%s%s/%%s/refunds",
constants.VERSION_V1,
constants.PAYMENT_URL,
)
// Define test response for successful multiple refunds fetch
successfulMultipleRefundsResp := map[string]interface{}{
"entity": "collection",
"count": float64(2),
"items": []interface{}{
map[string]interface{}{
"id": "rfnd_FP8DDKxqJif6ca",
"entity": "refund",
"amount": float64(300100),
"currency": "INR",
"payment_id": "pay_29QQoUBi66xm2f",
"notes": map[string]interface{}{
"comment": "Comment for refund",
},
"receipt": nil,
"acquirer_data": map[string]interface{}{
"arn": "10000000000000",
},
"created_at": float64(1597078124),
"batch_id": nil,
"status": "processed",
"speed_processed": "normal",
"speed_requested": "optimum",
},
map[string]interface{}{
"id": "rfnd_FP8DRfu3ygfOaC",
"entity": "refund",
"amount": float64(200000),
"currency": "INR",
"payment_id": "pay_29QQoUBi66xm2f",
"notes": map[string]interface{}{
"comment": "Comment for refund",
},
"receipt": nil,
"acquirer_data": map[string]interface{}{
"arn": "10000000000000",
},
"created_at": float64(1597078137),
"batch_id": nil,
"status": "processed",
"speed_processed": "normal",
"speed_requested": "optimum",
},
},
}
// Define error responses
errorResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "Bad request",
},
}
tests := []RazorpayToolTestCase{
{
Name: "fetch multiple refunds with query params",
Request: map[string]interface{}{
"payment_id": "pay_29QQoUBi66xm2f",
"from": 1500826740,
"to": 1500826760,
"count": 10,
"skip": 0,
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
fetchMultipleRefundsPathFmt,
"pay_29QQoUBi66xm2f",
),
Method: "GET",
Response: successfulMultipleRefundsResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulMultipleRefundsResp,
},
{
Name: "fetch multiple refunds api error",
Request: map[string]interface{}{
"payment_id": "pay_invalid",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
fetchMultipleRefundsPathFmt,
"pay_invalid",
),
Method: "GET",
Response: errorResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching multiple refunds failed: Bad request",
},
{
Name: "missing payment_id parameter",
Request: map[string]interface{}{},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "missing required parameter: payment_id",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
// Missing payment_id parameter
"from": "not-a-number", // Wrong type for from
"to": "not-a-number", // Wrong type for to
"count": "not-a-number", // Wrong type for count
"skip": "not-a-number", // Wrong type for skip
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "Validation errors:\n- " +
"missing required parameter: payment_id\n- " +
"invalid parameter type: from\n- " +
"invalid parameter type: to\n- " +
"invalid parameter type: count\n- " +
"invalid parameter type: skip",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchMultipleRefundsForPayment, "Refund")
})
}
}
func Test_FetchSpecificRefundForPayment(t *testing.T) {
fetchSpecificRefundPathFmt := fmt.Sprintf(
"/%s%s/%%s/refunds/%%s",
constants.VERSION_V1,
constants.PAYMENT_URL,
)
// Define test response for successful specific refund fetch
successfulSpecificRefundResp := map[string]interface{}{
"id": "rfnd_AABBdHIieexn5c",
"entity": "refund",
"amount": float64(300100),
"currency": "INR",
"payment_id": "pay_FIKOnlyii5QGNx",
"notes": map[string]interface{}{
"comment": "Comment for refund",
},
"receipt": nil,
"acquirer_data": map[string]interface{}{"arn": "10000000000000"},
"created_at": float64(1597078124),
"batch_id": nil,
"status": "processed",
"speed_processed": "normal",
"speed_requested": "optimum",
}
// Define error responses
notFoundResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "The id provided does not exist",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful specific refund fetch",
Request: map[string]interface{}{
"payment_id": "pay_FIKOnlyii5QGNx",
"refund_id": "rfnd_AABBdHIieexn5c",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
fetchSpecificRefundPathFmt,
"pay_FIKOnlyii5QGNx",
"rfnd_AABBdHIieexn5c",
),
Method: "GET",
Response: successfulSpecificRefundResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulSpecificRefundResp,
},
{
Name: "refund id not found",
Request: map[string]interface{}{
"payment_id": "pay_FIKOnlyii5QGNx",
"refund_id": "rfnd_nonexistent",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(
fetchSpecificRefundPathFmt,
"pay_FIKOnlyii5QGNx",
"rfnd_nonexistent",
),
Method: "GET",
Response: notFoundResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching specific refund for payment failed: " +
"The id provided does not exist",
},
{
Name: "missing payment_id parameter",
Request: map[string]interface{}{
"refund_id": "rfnd_AABBdHIieexn5c",
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "missing required parameter: payment_id",
},
{
Name: "missing refund_id parameter",
Request: map[string]interface{}{
"payment_id": "pay_FIKOnlyii5QGNx",
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "missing required parameter: refund_id",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
// Missing both payment_id and refund_id parameters
"non_existent_param": 12345, // Additional parameter that doesn't exist
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "Validation errors:\n- " +
"missing required parameter: payment_id\n- " +
"missing required parameter: refund_id",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchSpecificRefundForPayment, "Refund")
})
}
}
func Test_FetchAllRefunds(t *testing.T) {
fetchAllRefundsPath := fmt.Sprintf(
"/%s%s",
constants.VERSION_V1,
constants.REFUND_URL,
)
// Define test response for successful refund fetch
successfulRefundsResp := map[string]interface{}{
"entity": "collection",
"count": float64(2),
"items": []interface{}{
map[string]interface{}{
"id": "rfnd_FFX6AnnIN3puqW",
"entity": "refund",
"amount": float64(88800),
"currency": "INR",
"payment_id": "pay_FFX5FdEYx8jPwA",
"notes": map[string]interface{}{
"comment": "Issuing an instant refund",
},
"receipt": nil,
"acquirer_data": map[string]interface{}{},
"created_at": float64(1594982363),
"batch_id": nil,
"status": "processed",
"speed_processed": "optimum",
"speed_requested": "optimum",
},
map[string]interface{}{
"id": "rfnd_EqWThTE7dd7utf",
"entity": "refund",
"amount": float64(6000),
"currency": "INR",
"payment_id": "pay_EpkFDYRirena0f",
"notes": map[string]interface{}{
"comment": "Issuing a normal refund",
},
"receipt": nil,
"acquirer_data": map[string]interface{}{
"arn": "10000000000000",
},
"created_at": float64(1589521675),
"batch_id": nil,
"status": "processed",
"speed_processed": "normal",
"speed_requested": "normal",
},
},
}
// Define error response
errorResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "Bad request",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful fetch with pagination parameters",
Request: map[string]interface{}{
"count": 2,
"skip": 1,
"from": 1589000000,
"to": 1595000000,
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllRefundsPath,
Method: "GET",
Response: successfulRefundsResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulRefundsResp,
},
{
Name: "fetch with API error",
Request: map[string]interface{}{},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllRefundsPath,
Method: "GET",
Response: errorResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching refunds failed",
},
{
Name: "multiple validation errors",
Request: map[string]interface{}{
"from": "not-a-number", // Wrong type for from
"to": "not-a-number", // Wrong type for to
"count": "not-a-number", // Wrong type for count
"skip": "not-a-number", // Wrong type for skip
},
MockHttpClient: nil,
ExpectError: true,
ExpectedErrMsg: "Validation errors:\n- " +
"invalid parameter type: from\n- " +
"invalid parameter type: to\n- " +
"invalid parameter type: count\n- " +
"invalid parameter type: skip",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchAllRefunds, "Refund")
})
}
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/settlements_test.go:
--------------------------------------------------------------------------------
```go
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_FetchSettlement(t *testing.T) {
fetchSettlementPathFmt := fmt.Sprintf(
"/%s%s/%%s",
constants.VERSION_V1,
constants.SETTLEMENT_URL,
)
settlementResp := map[string]interface{}{
"id": "setl_FNj7g2YS5J67Rz",
"entity": "settlement",
"amount": float64(9973635),
"status": "processed",
"fees": float64(471),
"tax": float64(72),
"utr": "1568176198",
"created_at": float64(1568176198),
}
settlementNotFoundResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "settlement not found",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful settlement fetch",
Request: map[string]interface{}{
"settlement_id": "setl_FNj7g2YS5J67Rz",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(fetchSettlementPathFmt, "setl_FNj7g2YS5J67Rz"),
Method: "GET",
Response: settlementResp,
},
)
},
ExpectError: false,
ExpectedResult: settlementResp,
},
{
Name: "settlement not found",
Request: map[string]interface{}{
"settlement_id": "setl_invalid",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(fetchSettlementPathFmt, "setl_invalid"),
Method: "GET",
Response: settlementNotFoundResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching settlement failed: settlement not found",
},
{
Name: "missing settlement_id parameter",
Request: map[string]interface{}{},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: settlement_id",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchSettlement, "Settlement")
})
}
}
func Test_FetchSettlementRecon(t *testing.T) {
fetchSettlementReconPath := fmt.Sprintf(
"/%s%s/recon/combined",
constants.VERSION_V1,
constants.SETTLEMENT_URL,
)
settlementReconResp := map[string]interface{}{
"entity": "collection",
"count": float64(1),
"items": []interface{}{
map[string]interface{}{
"entity": "settlement",
"settlement_id": "setl_FNj7g2YS5J67Rz",
"settlement_utr": "1568176198",
"amount": float64(9973635),
"settlement_type": "regular",
"settlement_status": "processed",
"created_at": float64(1568176198),
},
},
}
invalidParamsResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "missing required parameters",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful settlement reconciliation fetch",
Request: map[string]interface{}{
"year": float64(2022),
"month": float64(10),
"day": float64(15),
"count": float64(10),
"skip": float64(0),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchSettlementReconPath,
Method: "GET",
Response: settlementReconResp,
},
)
},
ExpectError: false,
ExpectedResult: settlementReconResp,
},
{
Name: "settlement reconciliation with required params only",
Request: map[string]interface{}{
"year": float64(2022),
"month": float64(10),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchSettlementReconPath,
Method: "GET",
Response: settlementReconResp,
},
)
},
ExpectError: false,
ExpectedResult: settlementReconResp,
},
{
Name: "settlement reconciliation with invalid params",
Request: map[string]interface{}{
"year": float64(2022),
// missing month parameter
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchSettlementReconPath,
Method: "GET",
Response: invalidParamsResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "missing required parameter: month",
},
{
Name: "missing required parameters",
Request: map[string]interface{}{},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: year",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchSettlementRecon, "Settlement Reconciliation")
})
}
}
func Test_FetchAllSettlements(t *testing.T) {
fetchAllSettlementsPath := fmt.Sprintf(
"/%s%s",
constants.VERSION_V1,
constants.SETTLEMENT_URL,
)
// Define the sample response for all settlements
settlementsResp := map[string]interface{}{
"entity": "collection",
"count": float64(2),
"items": []interface{}{
map[string]interface{}{
"id": "setl_FNj7g2YS5J67Rz",
"entity": "settlement",
"amount": float64(9973635),
"status": "processed",
},
map[string]interface{}{
"id": "setl_FJOp0jOWlalIvt",
"entity": "settlement",
"amount": float64(299114),
"status": "processed",
},
},
}
invalidParamsResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "from must be between 946684800 and 4765046400",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful settlements fetch with no parameters",
Request: map[string]interface{}{},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllSettlementsPath,
Method: "GET",
Response: settlementsResp,
},
)
},
ExpectError: false,
ExpectedResult: settlementsResp,
},
{
Name: "successful settlements fetch with pagination",
Request: map[string]interface{}{
"count": float64(10),
"skip": float64(0),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllSettlementsPath,
Method: "GET",
Response: settlementsResp,
},
)
},
ExpectError: false,
ExpectedResult: settlementsResp,
},
{
Name: "successful settlements fetch with date range",
Request: map[string]interface{}{
"from": float64(1609459200), // 2021-01-01
"to": float64(1640995199), // 2021-12-31
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllSettlementsPath,
Method: "GET",
Response: settlementsResp,
},
)
},
ExpectError: false,
ExpectedResult: settlementsResp,
},
{
Name: "settlements fetch with invalid timestamp",
Request: map[string]interface{}{
"from": float64(900000000), // Invalid timestamp (too early)
"to": float64(1600000000),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllSettlementsPath,
Method: "GET",
Response: invalidParamsResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching settlements failed: from must be " +
"between 946684800 and 4765046400",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchAllSettlements, "Settlements List")
})
}
}
func Test_CreateInstantSettlement(t *testing.T) {
createInstantSettlementPath := fmt.Sprintf(
"/%s%s/ondemand",
constants.VERSION_V1,
constants.SETTLEMENT_URL,
)
// Successful response with all parameters
successfulSettlementResp := map[string]interface{}{
"id": "setlod_FNj7g2YS5J67Rz",
"entity": "settlement.ondemand",
"amount_requested": float64(200000),
"amount_settled": float64(0),
"amount_pending": float64(199410),
"amount_reversed": float64(0),
"fees": float64(590),
"tax": float64(90),
"currency": "INR",
"settle_full_balance": false,
"status": "initiated",
"description": "Need this to make vendor payments.",
"notes": map[string]interface{}{
"notes_key_1": "Tea, Earl Grey, Hot",
"notes_key_2": "Tea, Earl Grey… decaf.",
},
"created_at": float64(1596771429),
}
// Error response for insufficient amount
insufficientAmountResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "Minimum amount that can be settled is ₹ 1.",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful settlement creation with all parameters",
Request: map[string]interface{}{
"amount": float64(200000),
"settle_full_balance": false,
"description": "Need this to make vendor payments.",
"notes": map[string]interface{}{
"notes_key_1": "Tea, Earl Grey, Hot",
"notes_key_2": "Tea, Earl Grey… decaf.",
},
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createInstantSettlementPath,
Method: "POST",
Response: successfulSettlementResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulSettlementResp,
},
{
Name: "settlement creation with required parameters only",
Request: map[string]interface{}{
"amount": float64(200000),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createInstantSettlementPath,
Method: "POST",
Response: successfulSettlementResp,
},
)
},
ExpectError: false,
ExpectedResult: successfulSettlementResp,
},
{
Name: "settlement creation with insufficient amount",
Request: map[string]interface{}{
"amount": float64(10), // Less than minimum
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: createInstantSettlementPath,
Method: "POST",
Response: insufficientAmountResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "creating instant settlement failed: Minimum amount that " +
"can be settled is ₹ 1.",
},
{
Name: "missing amount parameter",
Request: map[string]interface{}{},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: amount",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, CreateInstantSettlement, "Instant Settlement")
})
}
}
func Test_FetchAllInstantSettlements(t *testing.T) {
fetchAllInstantSettlementsPath := fmt.Sprintf(
"/%s%s/ondemand",
constants.VERSION_V1,
constants.SETTLEMENT_URL,
)
// Sample response for successful fetch without expanded payouts
basicSettlementListResp := map[string]interface{}{
"entity": "collection",
"count": float64(2),
"items": []interface{}{
map[string]interface{}{
"id": "setlod_FNj7g2YS5J67Rz",
"entity": "settlement.ondemand",
"amount_requested": float64(200000),
"amount_settled": float64(199410),
"amount_pending": float64(0),
"amount_reversed": float64(0),
"fees": float64(590),
"tax": float64(90),
"currency": "INR",
"settle_full_balance": false,
"status": "processed",
"description": "Need this to make vendor payments.",
"notes": map[string]interface{}{
"notes_key_1": "Tea, Earl Grey, Hot",
"notes_key_2": "Tea, Earl Grey… decaf.",
},
"created_at": float64(1596771429),
},
map[string]interface{}{
"id": "setlod_FJOp0jOWlalIvt",
"entity": "settlement.ondemand",
"amount_requested": float64(300000),
"amount_settled": float64(299114),
"amount_pending": float64(0),
"amount_reversed": float64(0),
"fees": float64(886),
"tax": float64(136),
"currency": "INR",
"settle_full_balance": false,
"status": "processed",
"description": "Need this to buy stock.",
"notes": map[string]interface{}{
"notes_key_1": "Tea, Earl Grey, Hot",
"notes_key_2": "Tea, Earl Grey… decaf.",
},
"created_at": float64(1595826576),
},
},
}
// Sample response with expanded payouts
expandedSettlementListResp := map[string]interface{}{
"entity": "collection",
"count": float64(2),
"items": []interface{}{
map[string]interface{}{
"id": "setlod_FNj7g2YS5J67Rz",
"entity": "settlement.ondemand",
"amount_requested": float64(200000),
"amount_settled": float64(199410),
"amount_pending": float64(0),
"amount_reversed": float64(0),
"fees": float64(590),
"tax": float64(90),
"currency": "INR",
"settle_full_balance": false,
"status": "processed",
"description": "Need this to make vendor payments.",
"notes": map[string]interface{}{
"notes_key_1": "Tea, Earl Grey, Hot",
"notes_key_2": "Tea, Earl Grey… decaf.",
},
"created_at": float64(1596771429),
"ondemand_payouts": []interface{}{
map[string]interface{}{
"id": "pout_FNj7g2YS5J67Rz",
"entity": "payout",
"amount": float64(199410),
"status": "processed",
},
},
},
map[string]interface{}{
"id": "setlod_FJOp0jOWlalIvt",
"entity": "settlement.ondemand",
"amount_requested": float64(300000),
"amount_settled": float64(299114),
"amount_pending": float64(0),
"amount_reversed": float64(0),
"fees": float64(886),
"tax": float64(136),
"currency": "INR",
"settle_full_balance": false,
"status": "processed",
"description": "Need this to buy stock.",
"notes": map[string]interface{}{
"notes_key_1": "Tea, Earl Grey, Hot",
"notes_key_2": "Tea, Earl Grey… decaf.",
},
"created_at": float64(1595826576),
"ondemand_payouts": []interface{}{
map[string]interface{}{
"id": "pout_FJOp0jOWlalIvt",
"entity": "payout",
"amount": float64(299114),
"status": "processed",
},
},
},
},
}
// Error response when parameters are invalid
invalidParamsResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "from must be between 946684800 and 4765046400",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful instant settlements fetch with no parameters",
Request: map[string]interface{}{},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllInstantSettlementsPath,
Method: "GET",
Response: basicSettlementListResp,
},
)
},
ExpectError: false,
ExpectedResult: basicSettlementListResp,
},
{
Name: "instant settlements fetch with pagination",
Request: map[string]interface{}{
"count": float64(10),
"skip": float64(0),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllInstantSettlementsPath,
Method: "GET",
Response: basicSettlementListResp,
},
)
},
ExpectError: false,
ExpectedResult: basicSettlementListResp,
},
{
Name: "instant settlements fetch with expanded payouts",
Request: map[string]interface{}{
"expand": []interface{}{"ondemand_payouts"},
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllInstantSettlementsPath,
Method: "GET",
Response: expandedSettlementListResp,
},
)
},
ExpectError: false,
ExpectedResult: expandedSettlementListResp,
},
{
Name: "instant settlements fetch with date range",
Request: map[string]interface{}{
"from": float64(1609459200), // 2021-01-01
"to": float64(1640995199), // 2021-12-31
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllInstantSettlementsPath,
Method: "GET",
Response: basicSettlementListResp,
},
)
},
ExpectError: false,
ExpectedResult: basicSettlementListResp,
},
{
Name: "instant settlements fetch with invalid timestamp",
Request: map[string]interface{}{
"from": float64(900000000), // Invalid timestamp (too early)
"to": float64(1600000000),
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fetchAllInstantSettlementsPath,
Method: "GET",
Response: invalidParamsResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching instant settlements failed: from must be " +
"between 946684800 and 4765046400",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchAllInstantSettlements, "Instant Settlements List")
})
}
}
func Test_FetchInstantSettlement(t *testing.T) {
fetchInstantSettlementPathFmt := fmt.Sprintf(
"/%s%s/ondemand/%%s",
constants.VERSION_V1,
constants.SETTLEMENT_URL,
)
instantSettlementResp := map[string]interface{}{
"id": "setlod_FNj7g2YS5J67Rz",
"entity": "settlement.ondemand",
"amount_requested": float64(200000),
"amount_settled": float64(199410),
"amount_pending": float64(0),
"amount_reversed": float64(0),
"fees": float64(590),
"tax": float64(90),
"currency": "INR",
"settle_full_balance": false,
"status": "processed",
"description": "Need this to make vendor payments.",
"notes": map[string]interface{}{
"notes_key_1": "Tea, Earl Grey, Hot",
"notes_key_2": "Tea, Earl Grey… decaf.",
},
"created_at": float64(1596771429),
}
instantSettlementNotFoundResp := map[string]interface{}{
"error": map[string]interface{}{
"code": "BAD_REQUEST_ERROR",
"description": "instant settlement not found",
},
}
tests := []RazorpayToolTestCase{
{
Name: "successful instant settlement fetch",
Request: map[string]interface{}{
"settlement_id": "setlod_FNj7g2YS5J67Rz",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(fetchInstantSettlementPathFmt,
"setlod_FNj7g2YS5J67Rz"),
Method: "GET",
Response: instantSettlementResp,
},
)
},
ExpectError: false,
ExpectedResult: instantSettlementResp,
},
{
Name: "instant settlement not found",
Request: map[string]interface{}{
"settlement_id": "setlod_invalid",
},
MockHttpClient: func() (*http.Client, *httptest.Server) {
return mock.NewHTTPClient(
mock.Endpoint{
Path: fmt.Sprintf(fetchInstantSettlementPathFmt, "setlod_invalid"),
Method: "GET",
Response: instantSettlementNotFoundResp,
},
)
},
ExpectError: true,
ExpectedErrMsg: "fetching instant settlement failed: " +
"instant settlement not found",
},
{
Name: "missing settlement_id parameter",
Request: map[string]interface{}{},
MockHttpClient: nil, // No HTTP client needed for validation error
ExpectError: true,
ExpectedErrMsg: "missing required parameter: settlement_id",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
runToolTest(t, tc, FetchInstantSettlement, "Instant Settlement")
})
}
}
```