#
tokens: 16814/50000 2/59 files (page 3/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 4. Use http://codebase.md/razorpay/razorpay-mcp-server?lines=false&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       └── new-tool-from-docs.mdc
├── .cursorignore
├── .dockerignore
├── .github
│   ├── 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.go
│       └── stdio.go
├── codecov.yml
├── CONTRIBUTING.md
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── pkg
│   ├── contextkey
│   │   └── context_key.go
│   ├── log
│   │   ├── config.go
│   │   ├── log.go
│   │   ├── slog_test.go
│   │   └── slog.go
│   ├── mcpgo
│   │   ├── README.md
│   │   ├── server.go
│   │   ├── stdio.go
│   │   ├── tool.go
│   │   └── transport.go
│   ├── observability
│   │   └── 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.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.go
├── README.md
└── SECURITY.md
```

# Files

--------------------------------------------------------------------------------
/pkg/razorpay/payments.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

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

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

// FetchPayment returns a tool that fetches payment details using payment_id
func FetchPayment(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("payment_id is unique identifier "+
				"of the payment 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
		}

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

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "payment_id")

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

		paymentId := params["payment_id"].(string)

		payment, err := client.Payment.Fetch(paymentId, nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching payment failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(payment)
	}

	return mcpgo.NewTool(
		"fetch_payment",
		"Use this tool to retrieve the details of a specific payment "+
			"using its id. Amount returned is in paisa",
		parameters,
		handler,
	)
}

// FetchPaymentCardDetails returns a tool that fetches card details
// for a payment
func FetchPaymentCardDetails(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("Unique identifier of the payment for which "+
				"you want to retrieve card details. Must start with 'pay_'"),
			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
		}

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

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "payment_id")

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

		paymentId := params["payment_id"].(string)

		cardDetails, err := client.Payment.FetchCardDetails(
			paymentId, nil, nil)

		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching card details failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(cardDetails)
	}

	return mcpgo.NewTool(
		"fetch_payment_card_details",
		"Use this tool to retrieve the details of the card used to make a payment. "+
			"Only works for payments made using a card.",
		parameters,
		handler,
	)
}

// UpdatePayment returns a tool that updates the notes for a payment
func UpdatePayment(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("Unique identifier of the payment to be updated. "+
				"Must start with 'pay_'"),
			mcpgo.Required(),
		),
		mcpgo.WithObject(
			"notes",
			mcpgo.Description("Key-value pairs that can be used to store additional "+
				"information about the payment. Values must be strings or integers."),
			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
		}

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

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "payment_id").
			ValidateAndAddRequiredMap(paymentUpdateReq, "notes")

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

		paymentId := params["payment_id"].(string)

		// Update the payment
		updatedPayment, err := client.Payment.Edit(paymentId, paymentUpdateReq, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("updating payment failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(updatedPayment)
	}

	return mcpgo.NewTool(
		"update_payment",
		"Use this tool to update the notes field of a payment. Notes are "+
			"key-value pairs that can be used to store additional information.", //nolint:lll
		parameters,
		handler,
	)
}

// CapturePayment returns a tool that captures an authorized payment
func CapturePayment(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("Unique identifier of the payment to be captured. Should start with 'pay_'"), //nolint:lll
			mcpgo.Required(),
		),
		mcpgo.WithNumber(
			"amount",
			mcpgo.Description("The amount to be captured (in paisa). "+
				"Should be equal to the authorized amount"),
			mcpgo.Required(),
		),
		mcpgo.WithString(
			"currency",
			mcpgo.Description("ISO code of the currency in which the payment "+
				"was made (e.g., INR)"),
			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
		}

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

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "payment_id").
			ValidateAndAddRequiredInt(params, "amount").
			ValidateAndAddRequiredString(paymentCaptureReq, "currency")

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

		paymentId := params["payment_id"].(string)
		amount := int(params["amount"].(int64))

		// Capture the payment
		payment, err := client.Payment.Capture(
			paymentId,
			amount,
			paymentCaptureReq,
			nil,
		)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("capturing payment failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(payment)
	}

	return mcpgo.NewTool(
		"capture_payment",
		"Use this tool to capture a previously authorized payment. Only payments with 'authorized' status can be captured", //nolint:lll
		parameters,
		handler,
	)
}

// FetchAllPayments returns a tool to fetch multiple payments with filtering and pagination
//
//nolint:lll
func FetchAllPayments(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		// Pagination parameters
		mcpgo.WithNumber(
			"count",
			mcpgo.Description("Number of payments to fetch "+
				"(default: 10, max: 100)"),
			mcpgo.Min(1),
			mcpgo.Max(100),
		),
		mcpgo.WithNumber(
			"skip",
			mcpgo.Description("Number of payments to skip (default: 0)"),
			mcpgo.Min(0),
		),
		// Time range filters
		mcpgo.WithNumber(
			"from",
			mcpgo.Description("Unix timestamp (in seconds) from when "+
				"payments are to be fetched"),
			mcpgo.Min(0),
		),
		mcpgo.WithNumber(
			"to",
			mcpgo.Description("Unix timestamp (in seconds) up till when "+
				"payments are to be fetched"),
			mcpgo.Min(0),
		),
	}

	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 query parameters map
		paymentListOptions := make(map[string]interface{})

		validator := NewValidator(&r).
			ValidateAndAddPagination(paymentListOptions).
			ValidateAndAddOptionalInt(paymentListOptions, "from").
			ValidateAndAddOptionalInt(paymentListOptions, "to")

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

		// Fetch all payments using Razorpay SDK
		payments, err := client.Payment.All(paymentListOptions, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("fetching payments failed: %s", err.Error())), nil
		}

		return mcpgo.NewToolResultJSON(payments)
	}

	return mcpgo.NewTool(
		"fetch_all_payments",
		"Fetch all payments with optional filtering and pagination",
		parameters,
		handler,
	)
}

// extractPaymentID extracts the payment ID from the payment response
func extractPaymentID(payment map[string]interface{}) string {
	if id, exists := payment["razorpay_payment_id"]; exists && id != nil {
		return id.(string)
	}
	return ""
}

// extractNextActions extracts all available actions from the payment response
func extractNextActions(
	payment map[string]interface{},
) []map[string]interface{} {
	var actions []map[string]interface{}
	if nextArray, exists := payment["next"]; exists && nextArray != nil {
		if nextSlice, ok := nextArray.([]interface{}); ok {
			for _, item := range nextSlice {
				if nextItem, ok := item.(map[string]interface{}); ok {
					actions = append(actions, nextItem)
				}
			}
		}
	}
	return actions
}

// OTPResponse represents the response from OTP generation API

// sendOtp sends an OTP to the customer and returns the response
func sendOtp(otpUrl string) error {
	if otpUrl == "" {
		return fmt.Errorf("OTP URL is empty")
	}
	// Validate URL is safe and from Razorpay domain for security
	parsedURL, err := url.Parse(otpUrl)
	if err != nil {
		return fmt.Errorf("invalid OTP URL: %s", err.Error())
	}

	if parsedURL.Scheme != "https" {
		return fmt.Errorf("OTP URL must use HTTPS")
	}

	if !strings.Contains(parsedURL.Host, "razorpay.com") {
		return fmt.Errorf("OTP URL must be from Razorpay domain")
	}

	// Create a secure HTTP client with timeout
	client := &http.Client{
		Timeout: 10 * time.Second,
	}

	req, err := http.NewRequest("POST", otpUrl, nil)
	if err != nil {
		return fmt.Errorf("failed to create OTP request: %s", err.Error())
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("OTP generation failed: %s", err.Error())
	}
	defer resp.Body.Close()

	// Validate HTTP response status
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("OTP generation failed with HTTP status: %d",
			resp.StatusCode)
	}
	return nil
}

// buildInitiatePaymentResponse constructs the response for initiate payment
func buildInitiatePaymentResponse(
	payment map[string]interface{},
	paymentID string,
	actions []map[string]interface{},
) (map[string]interface{}, string) {
	response := map[string]interface{}{
		"razorpay_payment_id": paymentID,
		"payment_details":     payment,
		"status":              "payment_initiated",
		"message": "Payment initiated successfully using " +
			"S2S JSON v1 flow",
	}
	otpUrl := ""

	if len(actions) > 0 {
		response["available_actions"] = actions

		// Add guidance based on available actions
		var actionTypes []string
		hasOTP := false
		hasRedirect := false
		hasUPICollect := false
		hasUPIIntent := false

		for _, action := range actions {
			if actionType, exists := action["action"]; exists {
				actionStr := actionType.(string)
				actionTypes = append(actionTypes, actionStr)
				if actionStr == "otp_generate" {
					hasOTP = true
					otpUrl = action["url"].(string)
				}

				if actionStr == "redirect" {
					hasRedirect = true
				}

				if actionStr == "upi_collect" {
					hasUPICollect = true
				}

				if actionStr == "upi_intent" {
					hasUPIIntent = true
				}
			}
		}

		switch {
		case hasOTP:
			response["message"] = "Payment initiated. OTP authentication is " +
				"available. " +
				"Use the 'submit_otp' tool to submit OTP received by the customer " +
				"for authentication."
			addNextStepInstructions(response, paymentID)
		case hasRedirect:
			response["message"] = "Payment initiated. Redirect authentication is " +
				"available. Use the redirect URL provided in available_actions."
		case hasUPICollect:
			response["message"] = fmt.Sprintf(
				"Payment initiated. Available actions: %v", actionTypes)
		case hasUPIIntent:
			response["message"] = fmt.Sprintf(
				"Payment initiated. Available actions: %v", actionTypes)
		default:
			response["message"] = fmt.Sprintf(
				"Payment initiated. Available actions: %v", actionTypes)
		}
	} else {
		addFallbackNextStepInstructions(response, paymentID)
	}

	return response, otpUrl
}

// addNextStepInstructions adds next step guidance to the response
func addNextStepInstructions(
	response map[string]interface{},
	paymentID string,
) {
	if paymentID != "" {
		response["next_step"] = "Use 'resend_otp' to regenerate OTP or " +
			"'submit_otp' to proceed to enter OTP."
		response["next_tool"] = "resend_otp"
		response["next_tool_params"] = map[string]interface{}{
			"payment_id": paymentID,
		}
	}
}

// addFallbackNextStepInstructions adds fallback next step guidance
func addFallbackNextStepInstructions(
	response map[string]interface{},
	paymentID string,
) {
	if paymentID != "" {
		response["next_step"] = "Use 'resend_otp' to regenerate OTP or " +
			"'submit_otp' to proceed to enter OTP if " +
			"OTP authentication is required."
		response["next_tool"] = "resend_otp"
		response["next_tool_params"] = map[string]interface{}{
			"payment_id": paymentID,
		}
	}
}

// addContactAndEmailToPaymentData adds contact and email to payment data
func addContactAndEmailToPaymentData(
	paymentData map[string]interface{},
	params map[string]interface{},
) {
	// Add contact if provided
	if contact, exists := params["contact"]; exists && contact != "" {
		paymentData["contact"] = contact
	}

	// Add email if provided, otherwise generate from contact
	if email, exists := params["email"]; exists && email != "" {
		paymentData["email"] = email
	} else if contact, exists := paymentData["contact"]; exists && contact != "" {
		paymentData["email"] = contact.(string) + "@mcp.razorpay.com"
	}
}

// addAdditionalPaymentParameters adds additional parameters for UPI collect
// and other flows
func addAdditionalPaymentParameters(
	paymentData map[string]interface{},
	params map[string]interface{},
) {
	// Note: customer_id is now handled explicitly in buildPaymentData

	// Add method if provided
	if method, exists := params["method"]; exists && method != "" {
		paymentData["method"] = method
	}

	// Add save if provided
	if save, exists := params["save"]; exists {
		paymentData["save"] = save
	}

	// Add recurring if provided
	if recurring, exists := params["recurring"]; exists {
		paymentData["recurring"] = recurring
	}

	// Add UPI parameters if provided
	if upiParams, exists := params["upi"]; exists && upiParams != nil {
		if upiMap, ok := upiParams.(map[string]interface{}); ok {
			paymentData["upi"] = upiMap
		}
	}
}

// processUPIParameters handles VPA and UPI intent parameter processing
func processUPIParameters(params map[string]interface{}) {
	vpa, hasVPA := params["vpa"]
	upiIntent, hasUPIIntent := params["upi_intent"]

	// Handle VPA parameter (UPI collect flow)
	if hasVPA && vpa != "" {
		// Set method to UPI
		params["method"] = "upi"
		// Set UPI parameters for collect flow
		params["upi"] = map[string]interface{}{
			"flow":        "collect",
			"expiry_time": "6",
			"vpa":         vpa,
		}
	}

	// Handle UPI intent parameter (UPI intent flow)
	if hasUPIIntent && upiIntent == true {
		// Set method to UPI
		params["method"] = "upi"
		// Set UPI parameters for intent flow
		params["upi"] = map[string]interface{}{
			"flow": "intent",
		}
	}
}

// createOrGetCustomer creates or gets a customer if contact is provided
func createOrGetCustomer(
	client *rzpsdk.Client,
	params map[string]interface{},
) (map[string]interface{}, error) {
	contactValue, exists := params["contact"]
	if !exists || contactValue == "" {
		return nil, nil
	}

	contact := contactValue.(string)
	customerData := map[string]interface{}{
		"contact":       contact,
		"fail_existing": "0", // Get existing customer if exists
	}

	// Create/get customer using Razorpay SDK
	customer, err := client.Customer.Create(customerData, nil)
	if err != nil {
		return nil, fmt.Errorf(
			"failed to create/fetch customer with contact %s: %v",
			contact,
			err,
		)
	}
	return customer, nil
}

// buildPaymentData constructs the payment data for the API call
func buildPaymentData(
	params map[string]interface{},
	currency string,
	customerId string,
) *map[string]interface{} {
	paymentData := map[string]interface{}{
		"amount":   params["amount"],
		"currency": currency,
		"order_id": params["order_id"],
	}
	if customerId != "" {
		paymentData["customer_id"] = customerId
	}

	// Add token if provided (required for saved payment methods,
	// optional for UPI collect)
	if token, exists := params["token"]; exists && token != "" {
		paymentData["token"] = token
	}

	// Add contact and email parameters
	addContactAndEmailToPaymentData(paymentData, params)

	// Add additional parameters for UPI collect and other flows
	addAdditionalPaymentParameters(paymentData, params)

	// Add force_terminal_id if provided (for single block multiple debit orders)
	if terminalID, exists := params["force_terminal_id"]; exists &&
		terminalID != "" {
		paymentData["force_terminal_id"] = terminalID
	}

	return &paymentData
}

// processPaymentResult processes the payment creation result
func processPaymentResult(
	payment map[string]interface{},
) (map[string]interface{}, error) {
	// Extract payment ID and next actions from the response
	paymentID := extractPaymentID(payment)
	actions := extractNextActions(payment)

	// Build structured response using the helper function
	response, otpUrl := buildInitiatePaymentResponse(payment, paymentID, actions)

	// Only send OTP if there's an OTP URL
	if otpUrl != "" {
		err := sendOtp(otpUrl)
		if err != nil {
			return nil, fmt.Errorf("OTP generation failed: %s", err.Error())
		}
	}

	return response, nil
}

// InitiatePayment returns a tool that initiates a payment using order_id
// and token
// This implements the S2S JSON v1 flow for creating payments
func InitiatePayment(
	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 ₹100, use 10000)"),
			mcpgo.Required(),
			mcpgo.Min(100),
		),
		mcpgo.WithString(
			"currency",
			mcpgo.Description("Currency code for the payment. Default is 'INR'"),
		),
		mcpgo.WithString(
			"token",
			mcpgo.Description("Token ID of the saved payment method. "+
				"Must start with 'token_'"),
		),
		mcpgo.WithString(
			"order_id",
			mcpgo.Description("Order ID for which the payment is being initiated. "+
				"Must start with 'order_'"),
			mcpgo.Required(),
		),
		mcpgo.WithString(
			"email",
			mcpgo.Description("Customer's email address (optional)"),
		),
		mcpgo.WithString(
			"contact",
			mcpgo.Description("Customer's phone number"),
		),
		mcpgo.WithString(
			"customer_id",
			mcpgo.Description("Customer ID for the payment. "+
				"Must start with 'cust_'"),
		),
		mcpgo.WithBoolean(
			"save",
			mcpgo.Description("Whether to save the payment method for future use"),
		),
		mcpgo.WithString(
			"vpa",
			mcpgo.Description("Virtual Payment Address (VPA) for UPI payment. "+
				"When provided, automatically sets method='upi' and UPI parameters "+
				"with flow='collect' and expiry_time='6' (e.g., '9876543210@ptsbi')"),
		),
		mcpgo.WithBoolean(
			"upi_intent",
			mcpgo.Description("Enable UPI intent flow. "+
				"When set to true, automatically sets method='upi' and UPI parameters "+
				"with flow='intent'. The API will return a UPI URL in the response."),
		),
		mcpgo.WithBoolean(
			"recurring",
			mcpgo.Description("Set this to true for recurring payments like "+
				"single block multiple debit."),
		),
		mcpgo.WithString(
			"force_terminal_id",
			mcpgo.Description("Terminal ID to be passed in case of single block "+
				"multiple debit order."),
		),
	}

	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
		}

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

		validator := NewValidator(&r).
			ValidateAndAddRequiredInt(params, "amount").
			ValidateAndAddOptionalString(params, "currency").
			ValidateAndAddOptionalString(params, "token").
			ValidateAndAddRequiredString(params, "order_id").
			ValidateAndAddOptionalString(params, "email").
			ValidateAndAddOptionalString(params, "contact").
			ValidateAndAddOptionalString(params, "customer_id").
			ValidateAndAddOptionalBool(params, "save").
			ValidateAndAddOptionalString(params, "vpa").
			ValidateAndAddOptionalBool(params, "upi_intent").
			ValidateAndAddOptionalBool(params, "recurring").
			ValidateAndAddOptionalString(params, "force_terminal_id")

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

		// Set default currency
		currency := "INR"
		if c, exists := params["currency"]; exists && c != "" {
			currency = c.(string)
		}

		// Process UPI parameters (VPA for collect flow, upi_intent for intent flow)
		processUPIParameters(params)

		// Handle customer ID
		var customerID string
		if custID, exists := params["customer_id"]; exists && custID != "" {
			customerID = custID.(string)
		} else {
			// Create or get customer if contact is provided
			customer, err := createOrGetCustomer(client, params)
			if err != nil {
				return mcpgo.NewToolResultError(err.Error()), nil
			}
			if customer != nil {
				if id, ok := customer["id"].(string); ok {
					customerID = id
				}
			}
		}

		// Build payment data
		paymentDataPtr := buildPaymentData(params, currency, customerID)
		paymentData := *paymentDataPtr

		// Create payment using Razorpay SDK's CreatePaymentJson method
		// This follows the S2S JSON v1 flow:
		// https://api.razorpay.com/v1/payments/create/json
		payment, err := client.Payment.CreatePaymentJson(paymentData, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("initiating payment failed: %s", err.Error())), nil
		}

		// Process payment result
		response, err := processPaymentResult(payment)
		if err != nil {
			return mcpgo.NewToolResultError(err.Error()), nil
		}

		return mcpgo.NewToolResultJSON(response)
	}

	return mcpgo.NewTool(
		"initiate_payment",
		"Initiate a payment using the S2S JSON v1 flow. "+
			"Required parameters: amount and order_id. "+
			"For saved payment methods, provide token. "+
			"For UPI collect flow, provide 'vpa' parameter "+
			"which automatically sets UPI with flow='collect' and expiry_time='6'. "+
			"For UPI intent flow, set 'upi_intent=true' parameter "+
			"which automatically sets UPI with flow='intent' and API returns UPI URL. "+
			"Supports additional parameters like customer_id, email, "+
			"contact, save, and recurring. "+
			"Returns payment details including next action steps if required.",
		parameters,
		handler,
	)
}

// ResendOtp returns a tool that sends OTP for payment authentication
func ResendOtp(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("Unique identifier of the payment for which "+
				"OTP needs to be generated. Must start with 'pay_'"),
			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
		}

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

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "payment_id")

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

		paymentID := params["payment_id"].(string)

		// Resend OTP using Razorpay SDK
		otpResponse, err := client.Payment.OtpResend(paymentID, nil, nil)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("OTP resend failed: %s", err.Error())), nil
		}

		// Extract OTP submit URL from response
		otpSubmitURL := extractOtpSubmitURL(otpResponse)

		// Prepare response
		response := map[string]interface{}{
			"payment_id": paymentID,
			"status":     "success",
			"message": "OTP sent successfully. Please enter the OTP received on your " +
				"mobile number to complete the payment.",
			"response_data": otpResponse,
		}

		// Add next step instructions if OTP submit URL is available
		if otpSubmitURL != "" {
			response["otp_submit_url"] = otpSubmitURL
			response["next_step"] = "Use 'submit_otp' tool with the OTP code received " +
				"from user to complete payment authentication."
			response["next_tool"] = "submit_otp"
			response["next_tool_params"] = map[string]interface{}{
				"payment_id": paymentID,
				"otp_string": "{OTP_CODE_FROM_USER}",
			}
		} else {
			response["next_step"] = "Use 'submit_otp' tool with the OTP code received " +
				"from user to complete payment authentication."
			response["next_tool"] = "submit_otp"
			response["next_tool_params"] = map[string]interface{}{
				"payment_id": paymentID,
				"otp_string": "{OTP_CODE_FROM_USER}",
			}
		}

		result, err := mcpgo.NewToolResultJSON(response)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("JSON marshal error: %v", err)), nil
		}
		return result, nil
	}

	return mcpgo.NewTool(
		"resend_otp",
		"Resend OTP to the customer's registered mobile number if the previous "+
			"OTP was not received or has expired.",
		parameters,
		handler,
	)
}

// SubmitOtp returns a tool that submits OTP for payment verification
func SubmitOtp(
	obs *observability.Observability,
	client *rzpsdk.Client,
) mcpgo.Tool {
	parameters := []mcpgo.ToolParameter{
		mcpgo.WithString(
			"otp_string",
			mcpgo.Description("OTP string received from the user"),
			mcpgo.Required(),
		),
		mcpgo.WithString(
			"payment_id",
			mcpgo.Description("Unique identifier of the payment for which "+
				"OTP needs to be submitted. Must start with 'pay_'"),
			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
		}

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

		validator := NewValidator(&r).
			ValidateAndAddRequiredString(params, "otp_string").
			ValidateAndAddRequiredString(params, "payment_id")

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

		paymentID := params["payment_id"].(string)
		data := map[string]interface{}{
			"otp": params["otp_string"].(string),
		}
		otpResponse, err := client.Payment.OtpSubmit(paymentID, data, nil)

		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("OTP verification failed: %s", err.Error())), nil
		}

		// Prepare response
		response := map[string]interface{}{
			"payment_id":    paymentID,
			"status":        "success",
			"message":       "OTP verified successfully.",
			"response_data": otpResponse,
		}
		result, err := mcpgo.NewToolResultJSON(response)
		if err != nil {
			return mcpgo.NewToolResultError(
				fmt.Sprintf("JSON marshal error: %v", err)), nil
		}
		return result, nil
	}

	return mcpgo.NewTool(
		"submit_otp",
		"Verify and submit the OTP received by the customer to complete "+
			"the payment authentication process.",
		parameters,
		handler,
	)
}

// extractOtpSubmitURL extracts the OTP submit URL from the payment response
func extractOtpSubmitURL(responseData interface{}) string {
	jsonData, ok := responseData.(map[string]interface{})
	if !ok {
		return ""
	}

	nextArray, exists := jsonData["next"]
	if !exists || nextArray == nil {
		return ""
	}

	nextSlice, ok := nextArray.([]interface{})
	if !ok {
		return ""
	}

	for _, item := range nextSlice {
		nextItem, ok := item.(map[string]interface{})
		if !ok {
			continue
		}

		action, exists := nextItem["action"]
		if !exists || action != "otp_submit" {
			continue
		}

		submitURL, exists := nextItem["url"]
		if exists && submitURL != nil {
			if urlStr, ok := submitURL.(string); ok {
				return urlStr
			}
		}
	}

	return ""
}

```

--------------------------------------------------------------------------------
/pkg/razorpay/tools_params_test.go:
--------------------------------------------------------------------------------

```go
package razorpay

import (
	"testing"

	"github.com/stretchr/testify/assert"

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

func TestValidator(t *testing.T) {
	tests := []struct {
		name           string
		args           map[string]interface{}
		paramName      string
		validationFunc func(*Validator, map[string]interface{}, string) *Validator
		expectError    bool
		expectValue    interface{}
		expectKey      string
	}{
		// String tests
		{
			name:           "required string - valid",
			args:           map[string]interface{}{"test_param": "test_value"},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredString,
			expectError:    false,
			expectValue:    "test_value",
			expectKey:      "test_param",
		},
		{
			name:           "required string - missing",
			args:           map[string]interface{}{},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredString,
			expectError:    true,
			expectValue:    nil,
			expectKey:      "test_param",
		},
		{
			name:           "optional string - valid",
			args:           map[string]interface{}{"test_param": "test_value"},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalString,
			expectError:    false,
			expectValue:    "test_value",
			expectKey:      "test_param",
		},
		{
			name:           "optional string - empty",
			args:           map[string]interface{}{"test_param": ""},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalString,
			expectError:    false,
			expectValue:    "",
			expectKey:      "test_param",
		},

		// Int tests
		{
			name:           "required int - valid",
			args:           map[string]interface{}{"test_param": float64(123)},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredInt,
			expectError:    false,
			expectValue:    int64(123),
			expectKey:      "test_param",
		},
		{
			name:           "optional int - valid",
			args:           map[string]interface{}{"test_param": float64(123)},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalInt,
			expectError:    false,
			expectValue:    int64(123),
			expectKey:      "test_param",
		},
		{
			name:           "optional int - zero",
			args:           map[string]interface{}{"test_param": float64(0)},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalInt,
			expectError:    false,
			expectValue:    int64(0), // we expect the zero values as is
			expectKey:      "test_param",
		},

		// Float tests
		{
			name:           "required float - valid",
			args:           map[string]interface{}{"test_param": float64(123.45)},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredFloat,
			expectError:    false,
			expectValue:    float64(123.45),
			expectKey:      "test_param",
		},
		{
			name:           "optional float - valid",
			args:           map[string]interface{}{"test_param": float64(123.45)},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalFloat,
			expectError:    false,
			expectValue:    float64(123.45),
			expectKey:      "test_param",
		},
		{
			name:           "optional float - zero",
			args:           map[string]interface{}{"test_param": float64(0)},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalFloat,
			expectError:    false,
			expectValue:    float64(0),
			expectKey:      "test_param",
		},

		// Bool tests
		{
			name:           "required bool - true",
			args:           map[string]interface{}{"test_param": true},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredBool,
			expectError:    false,
			expectValue:    true,
			expectKey:      "test_param",
		},
		{
			name:           "required bool - false",
			args:           map[string]interface{}{"test_param": false},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredBool,
			expectError:    false,
			expectValue:    false,
			expectKey:      "test_param",
		},
		{
			name:           "optional bool - true",
			args:           map[string]interface{}{"test_param": true},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalBool,
			expectError:    false,
			expectValue:    true,
			expectKey:      "test_param",
		},
		{
			name:           "optional bool - false",
			args:           map[string]interface{}{"test_param": false},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalBool,
			expectError:    false,
			expectValue:    false,
			expectKey:      "test_param",
		},

		// Map tests
		{
			name: "required map - valid",
			args: map[string]interface{}{
				"test_param": map[string]interface{}{"key": "value"},
			},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredMap,
			expectError:    false,
			expectValue:    map[string]interface{}{"key": "value"},
			expectKey:      "test_param",
		},
		{
			name: "optional map - valid",
			args: map[string]interface{}{
				"test_param": map[string]interface{}{"key": "value"},
			},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalMap,
			expectError:    false,
			expectValue:    map[string]interface{}{"key": "value"},
			expectKey:      "test_param",
		},
		{
			name: "optional map - empty",
			args: map[string]interface{}{
				"test_param": map[string]interface{}{},
			},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalMap,
			expectError:    false,
			expectValue:    map[string]interface{}{},
			expectKey:      "test_param",
		},

		// Array tests
		{
			name: "required array - valid",
			args: map[string]interface{}{
				"test_param": []interface{}{"value1", "value2"},
			},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredArray,
			expectError:    false,
			expectValue:    []interface{}{"value1", "value2"},
			expectKey:      "test_param",
		},
		{
			name: "optional array - valid",
			args: map[string]interface{}{
				"test_param": []interface{}{"value1", "value2"},
			},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalArray,
			expectError:    false,
			expectValue:    []interface{}{"value1", "value2"},
			expectKey:      "test_param",
		},
		{
			name:           "optional array - empty",
			args:           map[string]interface{}{"test_param": []interface{}{}},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddOptionalArray,
			expectError:    false,
			expectValue:    []interface{}{},
			expectKey:      "test_param",
		},

		// Invalid type tests
		{
			name:           "required string - wrong type",
			args:           map[string]interface{}{"test_param": 123},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredString,
			expectError:    true,
			expectValue:    nil,
			expectKey:      "test_param",
		},
		{
			name:           "required int - wrong type",
			args:           map[string]interface{}{"test_param": "not a number"},
			paramName:      "test_param",
			validationFunc: (*Validator).ValidateAndAddRequiredInt,
			expectError:    true,
			expectValue:    nil,
			expectKey:      "test_param",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := make(map[string]interface{})
			request := &mcpgo.CallToolRequest{
				Arguments: tt.args,
			}
			validator := NewValidator(request)

			tt.validationFunc(validator, result, tt.paramName)

			if tt.expectError {
				assert.True(t, validator.HasErrors(), "Expected validation error")
			} else {
				assert.False(t, validator.HasErrors(), "Did not expect validation error")
				assert.Equal(t,
					tt.expectValue,
					result[tt.expectKey],
					"Parameter value mismatch",
				)
			}
		})
	}
}

func TestValidatorPagination(t *testing.T) {
	tests := []struct {
		name        string
		args        map[string]interface{}
		expectCount interface{}
		expectSkip  interface{}
		expectError bool
	}{
		{
			name: "valid pagination params",
			args: map[string]interface{}{
				"count": float64(10),
				"skip":  float64(5),
			},
			expectCount: int64(10),
			expectSkip:  int64(5),
			expectError: false,
		},
		{
			name:        "zero pagination params",
			args:        map[string]interface{}{"count": float64(0), "skip": float64(0)},
			expectCount: int64(0),
			expectSkip:  int64(0),
			expectError: false,
		},
		{
			name: "invalid count type",
			args: map[string]interface{}{
				"count": "not a number",
				"skip":  float64(5),
			},
			expectCount: nil,
			expectSkip:  int64(5),
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := make(map[string]interface{})
			request := &mcpgo.CallToolRequest{
				Arguments: tt.args,
			}
			validator := NewValidator(request)

			validator.ValidateAndAddPagination(result)

			if tt.expectError {
				assert.True(t, validator.HasErrors(), "Expected validation error")
			} else {
				assert.False(t, validator.HasErrors(), "Did not expect validation error")
			}

			if tt.expectCount != nil {
				assert.Equal(t, tt.expectCount, result["count"], "Count mismatch")
			} else {
				_, exists := result["count"]
				assert.False(t, exists, "Count should not be added")
			}

			if tt.expectSkip != nil {
				assert.Equal(t, tt.expectSkip, result["skip"], "Skip mismatch")
			} else {
				_, exists := result["skip"]
				assert.False(t, exists, "Skip should not be added")
			}
		})
	}
}

func TestValidatorExpand(t *testing.T) {
	tests := []struct {
		name         string
		args         map[string]interface{}
		expectExpand string
		expectError  bool
	}{
		{
			name:         "valid expand param",
			args:         map[string]interface{}{"expand": []interface{}{"payments"}},
			expectExpand: "payments",
			expectError:  false,
		},
		{
			name:         "empty expand array",
			args:         map[string]interface{}{"expand": []interface{}{}},
			expectExpand: "",
			expectError:  false,
		},
		{
			name:         "invalid expand type",
			args:         map[string]interface{}{"expand": "not an array"},
			expectExpand: "",
			expectError:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := make(map[string]interface{})
			request := &mcpgo.CallToolRequest{
				Arguments: tt.args,
			}
			validator := NewValidator(request)

			validator.ValidateAndAddExpand(result)

			if tt.expectError {
				assert.True(t, validator.HasErrors(), "Expected validation error")
			} else {
				assert.False(t, validator.HasErrors(), "Did not expect validation error")
				if tt.expectExpand != "" {
					assert.Equal(t,
						tt.expectExpand,
						result["expand[]"],
						"Expand value mismatch",
					)
				} else {
					_, exists := result["expand[]"]
					assert.False(t, exists, "Expand should not be added")
				}
			}
		})
	}
}

// Test validator "To" functions which write to target maps
func TestValidatorToFunctions(t *testing.T) {
	tests := []struct {
		name      string
		args      map[string]interface{}
		paramName string
		targetKey string
		testFunc  func(
			*Validator, map[string]interface{}, string, string,
		) *Validator
		expectValue interface{}
		expectError bool
	}{
		// ValidateAndAddOptionalStringToPath tests
		{
			name:        "optional string to target - valid",
			args:        map[string]interface{}{"customer_name": "Test User"},
			paramName:   "customer_name",
			targetKey:   "name",
			testFunc:    (*Validator).ValidateAndAddOptionalStringToPath,
			expectValue: "Test User",
			expectError: false,
		},
		{
			name:        "optional string to target - empty",
			args:        map[string]interface{}{"customer_name": ""},
			paramName:   "customer_name",
			targetKey:   "name",
			testFunc:    (*Validator).ValidateAndAddOptionalStringToPath,
			expectValue: "",
			expectError: false,
		},
		{
			name:        "optional string to target - missing",
			args:        map[string]interface{}{},
			paramName:   "customer_name",
			targetKey:   "name",
			testFunc:    (*Validator).ValidateAndAddOptionalStringToPath,
			expectValue: nil,
			expectError: false,
		},
		{
			name:        "optional string to target - wrong type",
			args:        map[string]interface{}{"customer_name": 123},
			paramName:   "customer_name",
			targetKey:   "name",
			testFunc:    (*Validator).ValidateAndAddOptionalStringToPath,
			expectValue: nil,
			expectError: true,
		},

		// ValidateAndAddOptionalBoolToPath tests
		{
			name:        "optional bool to target - true",
			args:        map[string]interface{}{"notify_sms": true},
			paramName:   "notify_sms",
			targetKey:   "sms",
			testFunc:    (*Validator).ValidateAndAddOptionalBoolToPath,
			expectValue: true,
			expectError: false,
		},
		{
			name:        "optional bool to target - false",
			args:        map[string]interface{}{"notify_sms": false},
			paramName:   "notify_sms",
			targetKey:   "sms",
			testFunc:    (*Validator).ValidateAndAddOptionalBoolToPath,
			expectValue: false,
			expectError: false,
		},
		{
			name:        "optional bool to target - wrong type",
			args:        map[string]interface{}{"notify_sms": "not a bool"},
			paramName:   "notify_sms",
			targetKey:   "sms",
			testFunc:    (*Validator).ValidateAndAddOptionalBoolToPath,
			expectValue: nil,
			expectError: true,
		},

		// ValidateAndAddOptionalIntToPath tests
		{
			name:        "optional int to target - valid",
			args:        map[string]interface{}{"age": float64(25)},
			paramName:   "age",
			targetKey:   "customer_age",
			testFunc:    (*Validator).ValidateAndAddOptionalIntToPath,
			expectValue: int64(25),
			expectError: false,
		},
		{
			name:        "optional int to target - zero",
			args:        map[string]interface{}{"age": float64(0)},
			paramName:   "age",
			targetKey:   "customer_age",
			testFunc:    (*Validator).ValidateAndAddOptionalIntToPath,
			expectValue: int64(0),
			expectError: false,
		},
		{
			name:        "optional int to target - missing",
			args:        map[string]interface{}{},
			paramName:   "age",
			targetKey:   "customer_age",
			testFunc:    (*Validator).ValidateAndAddOptionalIntToPath,
			expectValue: nil,
			expectError: false,
		},
		{
			name:        "optional int to target - wrong type",
			args:        map[string]interface{}{"age": "not a number"},
			paramName:   "age",
			targetKey:   "customer_age",
			testFunc:    (*Validator).ValidateAndAddOptionalIntToPath,
			expectValue: nil,
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create a target map for this specific test
			target := make(map[string]interface{})

			// Create the request and validator
			request := &mcpgo.CallToolRequest{
				Arguments: tt.args,
			}
			validator := NewValidator(request)

			// Call the test function with target and verify its return value
			tt.testFunc(validator, target, tt.paramName, tt.targetKey)

			// Check if we got the expected errors
			if tt.expectError {
				assert.True(t, validator.HasErrors(), "Expected validation error")
			} else {
				assert.False(t, validator.HasErrors(), "Did not expect validation error")

				// For non-error cases, check target map value
				if tt.expectValue != nil {
					// Should have the value with the target key
					assert.Equal(t,
						tt.expectValue,
						target[tt.targetKey],
						"Target map value mismatch")
				} else {
					// Target key should not exist
					_, exists := target[tt.targetKey]
					assert.False(t, exists, "Key should not be in target map when value is empty") // nolint:lll
				}
			}
		})
	}
}

// Test for nested validation with multiple fields into target maps
func TestValidatorNestedObjects(t *testing.T) {
	t.Run("customer object validation", func(t *testing.T) {
		// Create request with customer details
		args := map[string]interface{}{
			"customer_name":    "John Doe",
			"customer_email":   "[email protected]",
			"customer_contact": "+1234567890",
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		// Customer target map
		customer := make(map[string]interface{})

		// Create validator and validate customer fields
		validator := NewValidator(request).
			ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
			ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
			ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact")

		// Should not have errors
		assert.False(t, validator.HasErrors())

		// Customer map should have all three fields
		assert.Equal(t, "John Doe", customer["name"])
		assert.Equal(t, "[email protected]", customer["email"])
		assert.Equal(t, "+1234567890", customer["contact"])
	})

	t.Run("notification object validation", func(t *testing.T) {
		// Create request with notification settings
		args := map[string]interface{}{
			"notify_sms":   true,
			"notify_email": false,
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		// Notify target map
		notify := make(map[string]interface{})

		// Create validator and validate notification fields
		validator := NewValidator(request).
			ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
			ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email")

		// Should not have errors
		assert.False(t, validator.HasErrors())

		// Notify map should have both fields
		assert.Equal(t, true, notify["sms"])
		assert.Equal(t, false, notify["email"])
	})

	t.Run("mixed object with error", func(t *testing.T) {
		// Create request with mixed valid and invalid data
		args := map[string]interface{}{
			"customer_name":  "Jane Doe",
			"customer_email": 12345, // Wrong type
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		// Target map
		customer := make(map[string]interface{})

		// Create validator and validate fields
		validator := NewValidator(request).
			ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
			ValidateAndAddOptionalStringToPath(customer, "customer_email", "email")

		// Should have errors
		assert.True(t, validator.HasErrors())

		// Customer map should have only the valid field
		assert.Equal(t, "Jane Doe", customer["name"])
		_, hasEmail := customer["email"]
		assert.False(t, hasEmail, "Invalid field should not be added to target map")
	})
}

// Test for optional bool handling
func TestOptionalBoolBehavior(t *testing.T) {
	t.Run("explicit bool values", func(t *testing.T) {
		// Create request with explicit bool values
		args := map[string]interface{}{
			"true_param":  true,
			"false_param": false,
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		// Create result map
		result := make(map[string]interface{})

		// Validate both parameters
		validator := NewValidator(request).
			ValidateAndAddOptionalBool(result, "true_param").
			ValidateAndAddOptionalBool(result, "false_param")

		// Verify no errors occurred
		assert.False(t, validator.HasErrors())

		// Both parameters should be set in the result
		assert.Equal(t, true, result["true_param"])
		assert.Equal(t, false, result["false_param"])
	})

	t.Run("missing bool parameter", func(t *testing.T) {
		// Create request without bool parameters
		args := map[string]interface{}{
			"other_param": "some value",
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		// Create result map
		result := make(map[string]interface{})

		// Try to validate missing bool parameters
		validator := NewValidator(request).
			ValidateAndAddOptionalBool(result, "true_param").
			ValidateAndAddOptionalBool(result, "false_param")

		// Verify no errors occurred
		assert.False(t, validator.HasErrors())

		// Result should be empty since no bool values were provided
		assert.Empty(t, result)
	})

	t.Run("explicit bool values with 'To' functions", func(t *testing.T) {
		// Create request with explicit bool values
		args := map[string]interface{}{
			"notify_sms":   true,
			"notify_email": false,
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		// Create target map
		target := make(map[string]interface{})

		// Validate both parameters
		validator := NewValidator(request).
			ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms").
			ValidateAndAddOptionalBoolToPath(target, "notify_email", "email")

		// Verify no errors occurred
		assert.False(t, validator.HasErrors())

		// Both parameters should be set in the target map
		assert.Equal(t, true, target["sms"])
		assert.Equal(t, false, target["email"])
	})

	t.Run("missing bool parameter with 'To' functions", func(t *testing.T) {
		// Create request without bool parameters
		args := map[string]interface{}{
			"other_param": "some value",
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		// Create target map
		target := make(map[string]interface{})

		// Try to validate missing bool parameters
		validator := NewValidator(request).
			ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms").
			ValidateAndAddOptionalBoolToPath(target, "notify_email", "email")

		// Verify no errors occurred
		assert.False(t, validator.HasErrors())

		// Target map should be empty since no bool values were provided
		assert.Empty(t, target)
	})
}

// Test for extractValueGeneric function edge cases
func TestExtractValueGeneric(t *testing.T) {
	t.Run("invalid arguments type", func(t *testing.T) {
		request := &mcpgo.CallToolRequest{
			Arguments: "invalid_type", // Not a map
		}

		result, err := extractValueGeneric[string](request, "test", false)
		assert.Error(t, err)
		assert.Equal(t, "invalid arguments type", err.Error())
		assert.Nil(t, result)
	})

	t.Run("json marshal error", func(t *testing.T) {
		// Create a value that can't be marshaled to JSON
		args := map[string]interface{}{
			"test_param": make(chan int), // Channels can't be marshaled
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		result, err := extractValueGeneric[string](request, "test_param", false)
		assert.Error(t, err)
		assert.Equal(t, "invalid parameter type: test_param", err.Error())
		assert.Nil(t, result)
	})

	t.Run("json unmarshal error", func(t *testing.T) {
		// Provide a value that can't be unmarshaled to the target type
		args := map[string]interface{}{
			"test_param": []interface{}{1, 2, 3}, // Array can't be unmarshaled to string
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		result, err := extractValueGeneric[string](request, "test_param", false)
		assert.Error(t, err)
		assert.Equal(t, "invalid parameter type: test_param", err.Error())
		assert.Nil(t, result)
	})
}

// Test for validateAndAddRequired function
func TestValidateAndAddRequired(t *testing.T) {
	t.Run("successful validation", func(t *testing.T) {
		args := map[string]interface{}{
			"test_param": "test_value",
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddRequired[string](validator, params, "test_param")

		assert.False(t, result.HasErrors())
		assert.Equal(t, "test_value", params["test_param"])
	})

	t.Run("validation error", func(t *testing.T) {
		request := &mcpgo.CallToolRequest{
			Arguments: "invalid_type",
		}

		params := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddRequired[string](validator, params, "test_param")

		assert.True(t, result.HasErrors())
		assert.Empty(t, params)
	})

	t.Run("nil value after successful extraction", func(t *testing.T) {
		// This edge case is hard to trigger directly, but we can simulate it
		// by using a type that extractValueGeneric might return as nil
		args := map[string]interface{}{
			"test_param": nil,
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddRequired[string](validator, params, "test_param")

		// This should result in an error because the parameter is required
		assert.True(t, result.HasErrors())
		assert.Empty(t, params)
	})
}

// Test for validateAndAddOptional function
func TestValidateAndAddOptional(t *testing.T) {
	t.Run("successful validation", func(t *testing.T) {
		args := map[string]interface{}{
			"test_param": "test_value",
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddOptional[string](validator, params, "test_param")

		assert.False(t, result.HasErrors())
		assert.Equal(t, "test_value", params["test_param"])
	})

	t.Run("validation error", func(t *testing.T) {
		request := &mcpgo.CallToolRequest{
			Arguments: "invalid_type",
		}

		params := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddOptional[string](validator, params, "test_param")

		assert.True(t, result.HasErrors())
		assert.Empty(t, params)
	})

	t.Run("nil value handling", func(t *testing.T) {
		args := map[string]interface{}{
			"test_param": nil,
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddOptional[string](validator, params, "test_param")

		assert.False(t, result.HasErrors())
		assert.Empty(t, params)
	})
}

// Test for validateAndAddToPath function
func TestValidateAndAddToPath(t *testing.T) {
	t.Run("successful validation", func(t *testing.T) {
		args := map[string]interface{}{
			"test_param": "test_value",
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		target := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddToPath[string](
			validator, target, "test_param", "target_key")

		assert.False(t, result.HasErrors())
		assert.Equal(t, "test_value", target["target_key"])
	})

	t.Run("validation error", func(t *testing.T) {
		request := &mcpgo.CallToolRequest{
			Arguments: "invalid_type",
		}

		target := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddToPath[string](
			validator, target, "test_param", "target_key")

		assert.True(t, result.HasErrors())
		assert.Empty(t, target)
	})

	t.Run("nil value handling", func(t *testing.T) {
		args := map[string]interface{}{
			"test_param": nil,
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		target := make(map[string]interface{})
		validator := NewValidator(request)

		result := validateAndAddToPath[string](
			validator, target, "test_param", "target_key")

		assert.False(t, result.HasErrors())
		assert.Empty(t, target)
	})
}

// Test for ValidateAndAddPagination function
func TestValidateAndAddPagination(t *testing.T) {
	t.Run("all pagination parameters", func(t *testing.T) {
		args := map[string]interface{}{
			"count": 10,
			"skip":  5,
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddPagination(params)

		assert.False(t, validator.HasErrors())
		assert.Equal(t, int64(10), params["count"])
		assert.Equal(t, int64(5), params["skip"])
	})

	t.Run("missing pagination parameters", func(t *testing.T) {
		args := map[string]interface{}{}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddPagination(params)

		assert.False(t, validator.HasErrors())
		assert.Empty(t, params)
	})

	t.Run("invalid count type", func(t *testing.T) {
		args := map[string]interface{}{
			"count": "invalid",
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddPagination(params)

		assert.True(t, validator.HasErrors())
	})
}

// Test for ValidateAndAddExpand function
func TestValidateAndAddExpand(t *testing.T) {
	t.Run("valid expand parameter", func(t *testing.T) {
		args := map[string]interface{}{
			"expand": []string{"payments", "customer"},
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddExpand(params)

		assert.False(t, validator.HasErrors())
		// The function sets expand[] for each value, so check the last one
		assert.Equal(t, "customer", params["expand[]"])
	})

	t.Run("missing expand parameter", func(t *testing.T) {
		args := map[string]interface{}{}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddExpand(params)

		assert.False(t, validator.HasErrors())
		assert.Empty(t, params)
	})

	t.Run("invalid expand type", func(t *testing.T) {
		args := map[string]interface{}{
			"expand": "invalid", // Should be []string, not string
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddExpand(params)

		assert.True(t, validator.HasErrors())
	})
}

// Test for token validation functions edge cases
func TestTokenValidationEdgeCases(t *testing.T) {
	t.Run("validateTokenMaxAmount - int conversion", func(t *testing.T) {
		token := map[string]interface{}{
			"max_amount": 100, // int instead of float64
		}

		request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
		validator := NewValidator(request).validateTokenMaxAmount(token)

		assert.False(t, validator.HasErrors())
		assert.Equal(t, float64(100), token["max_amount"])
	})

	t.Run("validateTokenExpireAt - int conversion", func(t *testing.T) {
		token := map[string]interface{}{
			"expire_at": 1234567890, // int instead of float64
		}

		request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
		validator := NewValidator(request).validateTokenExpireAt(token)

		assert.False(t, validator.HasErrors())
		assert.Equal(t, float64(1234567890), token["expire_at"])
	})

	t.Run("validateTokenExpireAt - zero value", func(t *testing.T) {
		token := map[string]interface{}{
			"expire_at": 0,
		}

		request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
		validator := NewValidator(request).validateTokenExpireAt(token)

		assert.True(t, validator.HasErrors())
	})

	t.Run("validateTokenMaxAmount - zero value", func(t *testing.T) {
		token := map[string]interface{}{
			"max_amount": 0,
		}

		request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
		validator := NewValidator(request).validateTokenMaxAmount(token)

		assert.True(t, validator.HasErrors())
	})
}

// Test for ValidateAndAddToken edge cases
func TestValidateAndAddTokenEdgeCases(t *testing.T) {
	t.Run("token extraction error", func(t *testing.T) {
		request := &mcpgo.CallToolRequest{
			Arguments: "invalid_type",
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddToken(params, "token")

		assert.True(t, validator.HasErrors())
		assert.Empty(t, params)
	})

	t.Run("nil token value", func(t *testing.T) {
		args := map[string]interface{}{
			"token": nil,
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddToken(params, "token")

		assert.False(t, validator.HasErrors())
		assert.Empty(t, params)
	})

	t.Run("token validation errors", func(t *testing.T) {
		args := map[string]interface{}{
			"token": map[string]interface{}{
				"max_amount": -100, // Invalid value
			},
		}
		request := &mcpgo.CallToolRequest{
			Arguments: args,
		}

		params := make(map[string]interface{})
		validator := NewValidator(request).ValidateAndAddToken(params, "token")

		assert.True(t, validator.HasErrors())
		assert.Empty(t, params)
	})
}

```
Page 3/4FirstPrevNextLast