This is page 2 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/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/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") }) } } ``` -------------------------------------------------------------------------------- /pkg/razorpay/qr_codes_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_CreateQRCode(t *testing.T) { createQRCodePath := fmt.Sprintf( "/%s%s", constants.VERSION_V1, constants.QRCODE_URL, ) qrCodeWithAllParamsResp := map[string]interface{}{ "id": "qr_HMsVL8HOpbMcjU", "entity": "qr_code", "created_at": float64(1623660301), "name": "Store Front Display", "usage": "single_use", "type": "upi_qr", "image_url": "https://rzp.io/i/BWcUVrLp", "payment_amount": float64(300), "status": "active", "description": "For Store 1", "fixed_amount": true, "payments_amount_received": float64(0), "payments_count_received": float64(0), "notes": map[string]interface{}{ "purpose": "Test UPI QR Code notes", }, "customer_id": "cust_HKsR5se84c5LTO", "close_by": float64(1681615838), } qrCodeWithRequiredParamsResp := map[string]interface{}{ "id": "qr_HMsVL8HOpbMcjU", "entity": "qr_code", "created_at": float64(1623660301), "usage": "multiple_use", "type": "upi_qr", "image_url": "https://rzp.io/i/BWcUVrLp", "status": "active", "fixed_amount": false, "payments_amount_received": float64(0), "payments_count_received": float64(0), } qrCodeWithoutPaymentAmountResp := map[string]interface{}{ "id": "qr_HMsVL8HOpbMcjU", "entity": "qr_code", "created_at": float64(1623660301), "name": "Store Front Display", "usage": "single_use", "type": "upi_qr", "image_url": "https://rzp.io/i/BWcUVrLp", "status": "active", "description": "For Store 1", "fixed_amount": false, "payments_amount_received": float64(0), "payments_count_received": float64(0), } errorResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "The type field is invalid", }, } tests := []RazorpayToolTestCase{ { Name: "successful QR code creation with all parameters", Request: map[string]interface{}{ "type": "upi_qr", "name": "Store Front Display", "usage": "single_use", "fixed_amount": true, "payment_amount": float64(300), "description": "For Store 1", "customer_id": "cust_HKsR5se84c5LTO", "close_by": float64(1681615838), "notes": map[string]interface{}{ "purpose": "Test UPI QR Code notes", }, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: createQRCodePath, Method: "POST", Response: qrCodeWithAllParamsResp, }, ) }, ExpectError: false, ExpectedResult: qrCodeWithAllParamsResp, }, { Name: "successful QR code creation with required params only", Request: map[string]interface{}{ "type": "upi_qr", "usage": "multiple_use", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: createQRCodePath, Method: "POST", Response: qrCodeWithRequiredParamsResp, }, ) }, ExpectError: false, ExpectedResult: qrCodeWithRequiredParamsResp, }, { Name: "successful QR code creation without payment amount", Request: map[string]interface{}{ "type": "upi_qr", "name": "Store Front Display", "usage": "single_use", "fixed_amount": false, "description": "For Store 1", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: createQRCodePath, Method: "POST", Response: qrCodeWithoutPaymentAmountResp, }, ) }, ExpectError: false, ExpectedResult: qrCodeWithoutPaymentAmountResp, }, { Name: "missing required type parameter", Request: map[string]interface{}{ "usage": "single_use", }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "missing required parameter: type", }, { Name: "missing required usage parameter", Request: map[string]interface{}{ "type": "upi_qr", }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "missing required parameter: usage", }, { Name: "validator error - invalid parameter type", Request: map[string]interface{}{ "type": 123, "usage": "single_use", }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "Validation errors", }, { Name: "fixed_amount true but payment_amount missing", Request: map[string]interface{}{ "type": "upi_qr", "usage": "single_use", "fixed_amount": true, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "payment_amount is required when fixed_amount is true", }, { Name: "invalid type parameter", Request: map[string]interface{}{ "type": "invalid_type", "usage": "single_use", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: createQRCodePath, Method: "POST", Response: errorResp, }, ) }, ExpectError: true, ExpectedErrMsg: "creating QR code failed: The type field is invalid", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, CreateQRCode, "QR Code") }) } } func Test_FetchAllQRCodes(t *testing.T) { qrCodesPath := fmt.Sprintf( "/%s%s", constants.VERSION_V1, constants.QRCODE_URL, ) allQRCodesResp := map[string]interface{}{ "entity": "collection", "count": float64(2), "items": []interface{}{ map[string]interface{}{ "id": "qr_HO2jGkWReVBMNu", "entity": "qr_code", "created_at": float64(1623914648), "name": "Store_1", "usage": "single_use", "type": "upi_qr", "image_url": "https://rzp.io/i/w2CEwYmkAu", "payment_amount": float64(300), "status": "active", "description": "For Store 1", "fixed_amount": true, "payments_amount_received": float64(0), "payments_count_received": float64(0), "notes": map[string]interface{}{ "purpose": "Test UPI QR Code notes", }, "customer_id": "cust_HKsR5se84c5LTO", "close_by": float64(1681615838), "closed_at": nil, "close_reason": nil, }, map[string]interface{}{ "id": "qr_HO2e0813YlchUn", "entity": "qr_code", "created_at": float64(1623914349), "name": "Acme Groceries", "usage": "multiple_use", "type": "upi_qr", "image_url": "https://rzp.io/i/X6QM7LL", "payment_amount": nil, "status": "closed", "description": "Buy fresh groceries", "fixed_amount": false, "payments_amount_received": float64(200), "payments_count_received": float64(1), "notes": map[string]interface{}{ "Branch": "Bangalore - Rajaji Nagar", }, "customer_id": "cust_HKsR5se84c5LTO", "close_by": float64(1625077799), "closed_at": float64(1623914515), "close_reason": "on_demand", }, }, } errorResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "The query parameters are invalid", }, } tests := []RazorpayToolTestCase{ { Name: "successful fetch all QR codes with no parameters", Request: map[string]interface{}{}, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: allQRCodesResp, }, ) }, ExpectError: false, ExpectedResult: allQRCodesResp, }, { Name: "successful fetch all QR codes with count parameter", Request: map[string]interface{}{ "count": float64(2), }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: allQRCodesResp, }, ) }, ExpectError: false, ExpectedResult: allQRCodesResp, }, { Name: "successful fetch all QR codes with pagination parameters", Request: map[string]interface{}{ "from": float64(1622000000), "to": float64(1625000000), "count": float64(2), "skip": float64(0), }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: allQRCodesResp, }, ) }, ExpectError: false, ExpectedResult: allQRCodesResp, }, { Name: "invalid parameters - caught by SDK", Request: map[string]interface{}{ "count": float64(-1), }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "The count value should be greater than or equal to 1", }, }, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching QR codes failed: " + "The count value should be greater than or equal to 1", }, { Name: "validator error - invalid count parameter type", Request: map[string]interface{}{ "count": "not-a-number", }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "Validation errors", }, { Name: "API error response", Request: map[string]interface{}{ "count": float64(1000), }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: errorResp, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching QR codes failed: The query parameters are invalid", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, FetchAllQRCodes, "QR Codes") }) } } func Test_FetchQRCodesByCustomerID(t *testing.T) { qrCodesPath := fmt.Sprintf( "/%s%s", constants.VERSION_V1, constants.QRCODE_URL, ) customerQRCodesResp := map[string]interface{}{ "entity": "collection", "count": float64(1), "items": []interface{}{ map[string]interface{}{ "id": "qr_HMsgvioW64f0vh", "entity": "qr_code", "created_at": float64(1623660959), "name": "Store_1", "usage": "single_use", "type": "upi_qr", "image_url": "https://rzp.io/i/DTa2eQR", "payment_amount": float64(300), "status": "active", "description": "For Store 1", "fixed_amount": true, "payments_amount_received": float64(0), "payments_count_received": float64(0), "notes": map[string]interface{}{ "purpose": "Test UPI QR Code notes", }, "customer_id": "cust_HKsR5se84c5LTO", "close_by": float64(1681615838), "closed_at": nil, "close_reason": nil, }, }, } errorResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "The id provided is not a valid id", }, } tests := []RazorpayToolTestCase{ { Name: "successful fetch QR codes by customer ID", Request: map[string]interface{}{ "customer_id": "cust_HKsR5se84c5LTO", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: customerQRCodesResp, }, ) }, ExpectError: false, ExpectedResult: customerQRCodesResp, }, { Name: "missing required customer_id parameter", Request: map[string]interface{}{}, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "missing required parameter: customer_id", }, { Name: "validator error - invalid customer_id parameter type", Request: map[string]interface{}{ "customer_id": 12345, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "invalid parameter type: customer_id", }, { Name: "API error - invalid customer ID", Request: map[string]interface{}{ "customer_id": "invalid_customer_id", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: errorResp, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching QR codes failed: " + "The id provided is not a valid id", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, FetchQRCodesByCustomerID, "QR Codes by Customer ID") }) } } func Test_FetchQRCodesByPaymentID(t *testing.T) { qrCodesPath := fmt.Sprintf( "/%s%s", constants.VERSION_V1, constants.QRCODE_URL, ) paymentQRCodesResp := map[string]interface{}{ "entity": "collection", "count": float64(1), "items": []interface{}{ map[string]interface{}{ "id": "qr_HMsqRoeVwKbwAF", "entity": "qr_code", "created_at": float64(1623661499), "name": "Fresh Groceries", "usage": "multiple_use", "type": "upi_qr", "image_url": "https://rzp.io/i/eI9XD54Q", "payment_amount": nil, "status": "active", "description": "Buy fresh groceries", "fixed_amount": false, "payments_amount_received": float64(1000), "payments_count_received": float64(1), "notes": []interface{}{}, "customer_id": "cust_HKsR5se84c5LTO", "close_by": float64(1624472999), "close_reason": nil, }, }, } errorResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "The id provided is not a valid id", }, } tests := []RazorpayToolTestCase{ { Name: "successful fetch QR codes by payment ID", Request: map[string]interface{}{ "payment_id": "pay_Di5iqCqA1WEHq6", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: paymentQRCodesResp, }, ) }, ExpectError: false, ExpectedResult: paymentQRCodesResp, }, { Name: "missing required payment_id parameter", Request: map[string]interface{}{}, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "missing required parameter: payment_id", }, { Name: "validator error - invalid payment_id parameter type", Request: map[string]interface{}{ "payment_id": 12345, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "invalid parameter type: payment_id", }, { Name: "API error - invalid payment ID", Request: map[string]interface{}{ "payment_id": "invalid_payment_id", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: qrCodesPath, Method: "GET", Response: errorResp, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching QR codes failed: " + "The id provided is not a valid id", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, FetchQRCodesByPaymentID, "QR Codes by Payment ID") }) } } func TestFetchQRCode(t *testing.T) { // Initialize necessary variables qrID := "qr_FuZIYx6rMbP6gs" apiPath := fmt.Sprintf( "/%s%s/%s", constants.VERSION_V1, constants.QRCODE_URL, qrID, ) // Successful response based on Razorpay docs successResponse := map[string]interface{}{ "id": qrID, "entity": "qr_code", "created_at": float64(1623915088), "name": "Store_1", "usage": "single_use", "type": "upi_qr", "image_url": "https://rzp.io/i/oCswTOcCo", "payment_amount": float64(300), "status": "active", "description": "For Store 1", "fixed_amount": true, "payments_amount_received": float64(0), "payments_count_received": float64(0), "notes": map[string]interface{}{ "purpose": "Test UPI QR Code notes", }, "customer_id": "cust_HKsR5se84c5LTO", "close_by": float64(1681615838), "closed_at": nil, "close_reason": nil, } errorResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "The QR code ID provided is invalid", }, } tests := []RazorpayToolTestCase{ { Name: "successful fetch QR code by ID", Request: map[string]interface{}{ "qr_code_id": qrID, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: apiPath, Method: "GET", Response: successResponse, }, ) }, ExpectError: false, ExpectedResult: successResponse, }, { Name: "missing required qr_code_id parameter", Request: map[string]interface{}{}, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "missing required parameter: qr_code_id", }, { Name: "validator error - invalid qr_code_id parameter type", Request: map[string]interface{}{ "qr_code_id": 12345, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "invalid parameter type: qr_code_id", }, { Name: "API error - invalid QR code ID", Request: map[string]interface{}{ "qr_code_id": qrID, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: apiPath, Method: "GET", Response: errorResp, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching QR code failed: " + "The QR code ID provided is invalid", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, FetchQRCode, "QR Code") }) } } func TestFetchPaymentsForQRCode(t *testing.T) { apiPath := "/" + constants.VERSION_V1 + constants.QRCODE_URL + "/qr_test123/payments" successResponse := map[string]interface{}{ "entity": "collection", "count": float64(2), "items": []interface{}{ map[string]interface{}{ "id": "pay_test123", "entity": "payment", "amount": float64(500), "currency": "INR", "status": "captured", "method": "upi", "amount_refunded": float64(0), "refund_status": nil, "captured": true, "description": "QRv2 Payment", "customer_id": "cust_test123", "created_at": float64(1623662800), }, map[string]interface{}{ "id": "pay_test456", "entity": "payment", "amount": float64(1000), "currency": "INR", "status": "refunded", "method": "upi", "amount_refunded": float64(1000), "refund_status": "full", "captured": true, "description": "QRv2 Payment", "customer_id": "cust_test123", "created_at": float64(1623661533), }, }, } tests := []RazorpayToolTestCase{ { Name: "successful fetch payments for QR code", Request: map[string]interface{}{ "qr_code_id": "qr_test123", "count": 10, "from": 1623661000, "to": 1623663000, "skip": 0, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: apiPath, Method: "GET", Response: successResponse, }, ) }, ExpectError: false, ExpectedResult: successResponse, }, { Name: "missing required parameter", Request: map[string]interface{}{ "count": 10, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "missing required parameter: qr_code_id", }, { Name: "invalid parameter type", Request: map[string]interface{}{ "qr_code_id": 123, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "invalid parameter type: qr_code_id", }, { Name: "API error", Request: map[string]interface{}{ "qr_code_id": "qr_test123", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: apiPath, Method: "GET", Response: map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "mock error", }, }, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching payments for QR code failed: mock error", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, FetchPaymentsForQRCode, "QR Code Payments") }) } } func TestCloseQRCode(t *testing.T) { successResponse := map[string]interface{}{ "id": "qr_HMsVL8HOpbMcjU", "entity": "qr_code", "created_at": float64(1623660301), "name": "Store_1", "usage": "single_use", "type": "upi_qr", "image_url": "https://rzp.io/i/BWcUVrLp", "payment_amount": float64(300), "status": "closed", "description": "For Store 1", "fixed_amount": true, "payments_amount_received": float64(0), "payments_count_received": float64(0), "notes": map[string]interface{}{ "purpose": "Test UPI QR Code notes", }, "customer_id": "cust_HKsR5se84c5LTO", "close_by": float64(1681615838), "closed_at": float64(1623660445), "close_reason": "on_demand", } baseAPIPath := fmt.Sprintf("/%s%s", constants.VERSION_V1, constants.QRCODE_URL) qrCodeID := "qr_HMsVL8HOpbMcjU" apiPath := fmt.Sprintf("%s/%s/close", baseAPIPath, qrCodeID) tests := []RazorpayToolTestCase{ { Name: "successful close QR code", Request: map[string]interface{}{ "qr_code_id": qrCodeID, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: apiPath, Method: "POST", Response: successResponse, }, ) }, ExpectError: false, ExpectedResult: successResponse, }, { Name: "missing required qr_code_id parameter", Request: map[string]interface{}{}, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "missing required parameter: qr_code_id", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, CloseQRCode, "QR Code") }) } } ``` -------------------------------------------------------------------------------- /pkg/razorpay/orders_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_CreateOrder(t *testing.T) { createOrderPath := fmt.Sprintf( "/%s%s", constants.VERSION_V1, constants.ORDER_URL, ) // Define common response maps to be reused orderWithAllParamsResp := map[string]interface{}{ "id": "order_EKwxwAgItmmXdp", "amount": float64(10000), "currency": "INR", "receipt": "receipt-123", "partial_payment": true, "first_payment_min_amount": float64(5000), "notes": map[string]interface{}{ "customer_name": "test-customer", "product_name": "test-product", }, "status": "created", } orderWithRequiredParamsResp := map[string]interface{}{ "id": "order_EKwxwAgItmmXdp", "amount": float64(10000), "currency": "INR", "status": "created", } errorResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "Razorpay API error: Bad request", }, } tests := []RazorpayToolTestCase{ { Name: "successful order creation with all parameters", Request: map[string]interface{}{ "amount": float64(10000), "currency": "INR", "receipt": "receipt-123", "partial_payment": true, "first_payment_min_amount": float64(5000), "notes": map[string]interface{}{ "customer_name": "test-customer", "product_name": "test-product", }, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: createOrderPath, Method: "POST", Response: orderWithAllParamsResp, }, ) }, ExpectError: false, ExpectedResult: orderWithAllParamsResp, }, { Name: "successful order creation with required params only", Request: map[string]interface{}{ "amount": float64(10000), "currency": "INR", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: createOrderPath, Method: "POST", Response: orderWithRequiredParamsResp, }, ) }, ExpectError: false, ExpectedResult: orderWithRequiredParamsResp, }, { Name: "multiple validation errors", Request: map[string]interface{}{ // Missing both amount and currency (required parameters) "partial_payment": "invalid_boolean", // Wrong type for boolean "first_payment_min_amount": "invalid_number", // Wrong type for number }, 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: partial_payment", }, { Name: "first_payment_min_amount validation when partial_payment is true", Request: map[string]interface{}{ "amount": float64(10000), "currency": "INR", "partial_payment": true, "first_payment_min_amount": "invalid_number", }, MockHttpClient: nil, // No HTTP client needed for validation error ExpectError: true, ExpectedErrMsg: "Validation errors:\n- " + "invalid parameter type: first_payment_min_amount", }, { Name: "order creation fails", Request: map[string]interface{}{ "amount": float64(10000), "currency": "INR", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: createOrderPath, Method: "POST", Response: errorResp, }, ) }, ExpectError: true, ExpectedErrMsg: "creating order failed: Razorpay API error: Bad request", }, { Name: "successful SBMD mandate order creation", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": "as_presented", "type": "single_block_multiple_debit", }, "receipt": "Receipt No. 1", "notes": map[string]interface{}{ "notes_key_1": "Tea, Earl Grey, Hot", "notes_key_2": "Tea, Earl Grey... decaf.", }, }, MockHttpClient: func() (*http.Client, *httptest.Server) { sbmdOrderResp := map[string]interface{}{ "id": "order_SBMD123456", "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": "as_presented", "type": "single_block_multiple_debit", }, "receipt": "Receipt No. 1", "status": "created", "notes": map[string]interface{}{ "notes_key_1": "Tea, Earl Grey, Hot", "notes_key_2": "Tea, Earl Grey... decaf.", }, } return mock.NewHTTPClient( mock.Endpoint{ Path: createOrderPath, Method: "POST", Response: sbmdOrderResp, }, ) }, ExpectError: false, ExpectedResult: map[string]interface{}{ "id": "order_SBMD123456", "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": "as_presented", "type": "single_block_multiple_debit", }, "receipt": "Receipt No. 1", "status": "created", "notes": map[string]interface{}{ "notes_key_1": "Tea, Earl Grey, Hot", "notes_key_2": "Tea, Earl Grey... decaf.", }, }, }, { Name: "mandate order with invalid token parameter type", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": "invalid_token_should_be_object", }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "invalid parameter type: token", }, { Name: "mandate order with invalid method parameter type", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": 123, "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": "as_presented", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "invalid parameter type: method", }, { Name: "token validation - missing max_amount", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "expire_at": float64(2709971120), "frequency": "as_presented", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.max_amount is required", }, { Name: "token validation - missing frequency", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.frequency is required", }, { Name: "token validation - invalid max_amount type", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": "invalid_string", "expire_at": float64(2709971120), "frequency": "as_presented", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.max_amount must be a number", }, { Name: "token validation - invalid max_amount value", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(-100), "expire_at": float64(2709971120), "frequency": "as_presented", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.max_amount must be greater than 0", }, { Name: "token validation - invalid expire_at type", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": "invalid_string", "frequency": "as_presented", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.expire_at must be a number", }, { Name: "token validation - invalid expire_at value", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(-100), "frequency": "as_presented", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.expire_at must be greater than 0", }, { Name: "token validation - invalid frequency type", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": 123, }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.frequency must be a string", }, { Name: "token validation - invalid frequency value", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": "invalid_frequency", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.frequency must be one of: as_presented, " + "monthly, one_time, yearly, weekly, daily", }, { Name: "token validation - invalid type value", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": "as_presented", "type": "invalid_type", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.type must be one of: single_block_multiple_debit", }, { Name: "token validation - invalid type type", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": "as_presented", "type": 123, }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.type must be a string", }, { Name: "token validation - missing type", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "expire_at": float64(2709971120), "frequency": "as_presented", }, }, MockHttpClient: nil, ExpectError: true, ExpectedErrMsg: "token.type is required", }, { Name: "token validation - default expire_at when not provided", Request: map[string]interface{}{ "amount": float64(500000), "currency": "INR", "customer_id": "cust_4xbQrmEoA5WJ01", "method": "upi", "token": map[string]interface{}{ "max_amount": float64(500000), "frequency": "as_presented", "type": "single_block_multiple_debit", }, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: createOrderPath, Method: "POST", Response: map[string]interface{}{ "id": "order_test_12345", }, }, ) }, ExpectError: false, ExpectedResult: map[string]interface{}{ "id": "order_test_12345", }, }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, CreateOrder, "Order") }) } } func Test_FetchOrder(t *testing.T) { fetchOrderPathFmt := fmt.Sprintf( "/%s%s/%%s", constants.VERSION_V1, constants.ORDER_URL, ) orderResp := map[string]interface{}{ "id": "order_EKwxwAgItmmXdp", "amount": float64(10000), "currency": "INR", "receipt": "receipt-123", "status": "created", } orderNotFoundResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "order not found", }, } tests := []RazorpayToolTestCase{ { Name: "successful order fetch", Request: map[string]interface{}{ "order_id": "order_EKwxwAgItmmXdp", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fmt.Sprintf(fetchOrderPathFmt, "order_EKwxwAgItmmXdp"), Method: "GET", Response: orderResp, }, ) }, ExpectError: false, ExpectedResult: orderResp, }, { Name: "order not found", Request: map[string]interface{}{ "order_id": "order_invalid", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fmt.Sprintf(fetchOrderPathFmt, "order_invalid"), Method: "GET", Response: orderNotFoundResp, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching order failed: order not found", }, { Name: "missing order_id parameter", Request: map[string]interface{}{}, MockHttpClient: nil, // No HTTP client needed for validation error ExpectError: true, ExpectedErrMsg: "missing required parameter: order_id", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, FetchOrder, "Order") }) } } func Test_FetchAllOrders(t *testing.T) { fetchAllOrdersPath := fmt.Sprintf( "/%s%s", constants.VERSION_V1, constants.ORDER_URL, ) // Define the sample response for all orders ordersResp := map[string]interface{}{ "entity": "collection", "count": float64(2), "items": []interface{}{ map[string]interface{}{ "id": "order_EKzX2WiEWbMxmx", "entity": "order", "amount": float64(1234), "amount_paid": float64(0), "amount_due": float64(1234), "currency": "INR", "receipt": "Receipt No. 1", "offer_id": nil, "status": "created", "attempts": float64(0), "notes": []interface{}{}, "created_at": float64(1582637108), }, map[string]interface{}{ "id": "order_EAI5nRfThga2TU", "entity": "order", "amount": float64(100), "amount_paid": float64(0), "amount_due": float64(100), "currency": "INR", "receipt": "Receipt No. 1", "offer_id": nil, "status": "created", "attempts": float64(0), "notes": []interface{}{}, "created_at": float64(1580300731), }, }, } // Define error response errorResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "Razorpay API error: Bad request", }, } // Define the test cases tests := []RazorpayToolTestCase{ { Name: "successful fetch all orders with no parameters", Request: map[string]interface{}{}, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fetchAllOrdersPath, Method: "GET", Response: ordersResp, }, ) }, ExpectError: false, ExpectedResult: ordersResp, }, { Name: "successful fetch all orders with pagination", Request: map[string]interface{}{ "count": 2, "skip": 1, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fetchAllOrdersPath, Method: "GET", Response: ordersResp, }, ) }, ExpectError: false, ExpectedResult: ordersResp, }, { Name: "successful fetch all orders with time range", Request: map[string]interface{}{ "from": 1580000000, "to": 1590000000, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fetchAllOrdersPath, Method: "GET", Response: ordersResp, }, ) }, ExpectError: false, ExpectedResult: ordersResp, }, { Name: "successful fetch all orders with filtering", Request: map[string]interface{}{ "authorized": 1, "receipt": "Receipt No. 1", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fetchAllOrdersPath, Method: "GET", Response: ordersResp, }, ) }, ExpectError: false, ExpectedResult: ordersResp, }, { Name: "successful fetch all orders with expand", Request: map[string]interface{}{ "expand": []interface{}{"payments"}, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fetchAllOrdersPath, Method: "GET", Response: ordersResp, }, ) }, ExpectError: false, ExpectedResult: ordersResp, }, { Name: "multiple validation errors", Request: map[string]interface{}{ "count": "not-a-number", "skip": "not-a-number", "from": "not-a-number", "to": "not-a-number", "expand": "not-an-array", }, MockHttpClient: nil, // No HTTP client needed for validation error ExpectError: true, ExpectedErrMsg: "Validation errors:\n- " + "invalid parameter type: count\n- " + "invalid parameter type: skip\n- " + "invalid parameter type: from\n- " + "invalid parameter type: to\n- " + "invalid parameter type: expand", }, { Name: "fetch all orders fails", Request: map[string]interface{}{ "count": 100, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fetchAllOrdersPath, Method: "GET", Response: errorResp, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching orders failed: Razorpay API error: Bad request", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, FetchAllOrders, "Order") }) } } func Test_FetchOrderPayments(t *testing.T) { fetchOrderPaymentsPathFmt := fmt.Sprintf( "/%s%s/%%s/payments", constants.VERSION_V1, constants.ORDER_URL, ) // Define the sample response for order payments paymentsResp := map[string]interface{}{ "entity": "collection", "count": float64(2), "items": []interface{}{ map[string]interface{}{ "id": "pay_N8FUmetkCE2hZP", "entity": "payment", "amount": float64(100), "currency": "INR", "status": "failed", "order_id": "order_N8FRN5zTm5S3wx", "invoice_id": nil, "international": false, "method": "upi", "amount_refunded": float64(0), "refund_status": nil, "captured": false, "description": nil, "card_id": nil, "bank": nil, "wallet": nil, "vpa": "failure@razorpay", "email": "[email protected]", "contact": "+919999999999", "notes": map[string]interface{}{ "notes_key_1": "Tea, Earl Grey, Hot", "notes_key_2": "Tea, Earl Grey… decaf.", }, "fee": nil, "tax": nil, "error_code": "BAD_REQUEST_ERROR", "error_description": "Payment was unsuccessful due to a temporary issue.", "error_source": "gateway", "error_step": "payment_response", "error_reason": "payment_failed", "acquirer_data": map[string]interface{}{ "rrn": nil, }, "created_at": float64(1701688684), "upi": map[string]interface{}{ "vpa": "failure@razorpay", }, }, map[string]interface{}{ "id": "pay_N8FVRD1DzYzBh1", "entity": "payment", "amount": float64(100), "currency": "INR", "status": "captured", "order_id": "order_N8FRN5zTm5S3wx", "invoice_id": nil, "international": false, "method": "upi", "amount_refunded": float64(0), "refund_status": nil, "captured": true, "description": nil, "card_id": nil, "bank": nil, "wallet": nil, "vpa": "success@razorpay", "email": "[email protected]", "contact": "+919999999999", "notes": map[string]interface{}{ "notes_key_1": "Tea, Earl Grey, Hot", "notes_key_2": "Tea, Earl Grey… decaf.", }, "fee": float64(2), "tax": float64(0), "error_code": nil, "error_description": nil, "error_source": nil, "error_step": nil, "error_reason": nil, "acquirer_data": map[string]interface{}{ "rrn": "267567962619", "upi_transaction_id": "F5B66C7C07CA6FEAD77E956DC2FC7ABE", }, "created_at": float64(1701688721), "upi": map[string]interface{}{ "vpa": "success@razorpay", }, }, }, } orderNotFoundResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "order not found", }, } tests := []RazorpayToolTestCase{ { Name: "successful fetch of order payments", Request: map[string]interface{}{ "order_id": "order_N8FRN5zTm5S3wx", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fmt.Sprintf( fetchOrderPaymentsPathFmt, "order_N8FRN5zTm5S3wx", ), Method: "GET", Response: paymentsResp, }, ) }, ExpectError: false, ExpectedResult: paymentsResp, }, { Name: "order not found", Request: map[string]interface{}{ "order_id": "order_invalid", }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fmt.Sprintf( fetchOrderPaymentsPathFmt, "order_invalid", ), Method: "GET", Response: orderNotFoundResp, }, ) }, ExpectError: true, ExpectedErrMsg: "fetching payments for order failed: order not found", }, { Name: "missing order_id parameter", Request: map[string]interface{}{}, MockHttpClient: nil, // No HTTP client needed for validation error ExpectError: true, ExpectedErrMsg: "missing required parameter: order_id", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, FetchOrderPayments, "Order") }) } } func Test_UpdateOrder(t *testing.T) { updateOrderPathFmt := fmt.Sprintf( "/%s%s/%%s", constants.VERSION_V1, constants.ORDER_URL, ) updatedOrderResp := map[string]interface{}{ "id": "order_EKwxwAgItmmXdp", "entity": "order", "amount": float64(10000), "currency": "INR", "receipt": "receipt-123", "status": "created", "attempts": float64(0), "created_at": float64(1572505143), "notes": map[string]interface{}{ "customer_name": "updated-customer", "product_name": "updated-product", }, } orderNotFoundResp := map[string]interface{}{ "error": map[string]interface{}{ "code": "BAD_REQUEST_ERROR", "description": "order not found", }, } tests := []RazorpayToolTestCase{ { Name: "successful order update", Request: map[string]interface{}{ "order_id": "order_EKwxwAgItmmXdp", "notes": map[string]interface{}{ "customer_name": "updated-customer", "product_name": "updated-product", }, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fmt.Sprintf( updateOrderPathFmt, "order_EKwxwAgItmmXdp"), Method: "PATCH", Response: updatedOrderResp, }, ) }, ExpectError: false, ExpectedResult: updatedOrderResp, }, { Name: "missing required parameters - order_id", Request: map[string]interface{}{ // Missing order_id "notes": map[string]interface{}{ "customer_name": "updated-customer", "product_name": "updated-product", }, }, MockHttpClient: nil, // No HTTP client needed for validation error ExpectError: true, ExpectedErrMsg: "missing required parameter: order_id", }, { Name: "missing required parameters - notes", Request: map[string]interface{}{ "order_id": "order_EKwxwAgItmmXdp", // Missing notes }, MockHttpClient: nil, // No HTTP client needed for validation error ExpectError: true, ExpectedErrMsg: "missing required parameter: notes", }, { Name: "order not found", Request: map[string]interface{}{ "order_id": "order_invalid_id", "notes": map[string]interface{}{ "customer_name": "updated-customer", "product_name": "updated-product", }, }, MockHttpClient: func() (*http.Client, *httptest.Server) { return mock.NewHTTPClient( mock.Endpoint{ Path: fmt.Sprintf(updateOrderPathFmt, "order_invalid_id"), Method: "PATCH", Response: orderNotFoundResp, }, ) }, ExpectError: true, ExpectedErrMsg: "updating order failed: order not found", }, } for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { runToolTest(t, tc, UpdateOrder, "Order") }) } } ```