#
tokens: 45939/50000 10/69 files (page 2/4)
lines: off (toggle) GitHub
raw markdown copy
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")
		})
	}
}

```
Page 2/4FirstPrevNextLast