This is page 3 of 4. Use http://codebase.md/razorpay/razorpay-mcp-server?page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ └── new-tool-from-docs.mdc
├── .cursorignore
├── .dockerignore
├── .github
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── pull_request_template.md
│ └── workflows
│ ├── assign.yml
│ ├── build.yml
│ ├── ci.yml
│ ├── docker-publish.yml
│ ├── lint.yml
│ └── release.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── cmd
│ └── razorpay-mcp-server
│ ├── main_test.go
│ ├── main.go
│ ├── stdio_test.go
│ └── stdio.go
├── codecov.yml
├── CONTRIBUTING.md
├── coverage.out
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── pkg
│ ├── contextkey
│ │ ├── context_key_test.go
│ │ └── context_key.go
│ ├── log
│ │ ├── config_test.go
│ │ ├── config.go
│ │ ├── log.go
│ │ ├── slog_test.go
│ │ └── slog.go
│ ├── mcpgo
│ │ ├── README.md
│ │ ├── server_test.go
│ │ ├── server.go
│ │ ├── stdio_test.go
│ │ ├── stdio.go
│ │ ├── tool_test.go
│ │ ├── tool.go
│ │ └── transport.go
│ ├── observability
│ │ ├── observability_test.go
│ │ └── observability.go
│ ├── razorpay
│ │ ├── mock
│ │ │ ├── server_test.go
│ │ │ └── server.go
│ │ ├── orders_test.go
│ │ ├── orders.go
│ │ ├── payment_links_test.go
│ │ ├── payment_links.go
│ │ ├── payments_test.go
│ │ ├── payments.go
│ │ ├── payouts_test.go
│ │ ├── payouts.go
│ │ ├── qr_codes_test.go
│ │ ├── qr_codes.go
│ │ ├── README.md
│ │ ├── refunds_test.go
│ │ ├── refunds.go
│ │ ├── server_test.go
│ │ ├── server.go
│ │ ├── settlements_test.go
│ │ ├── settlements.go
│ │ ├── test_helpers.go
│ │ ├── tokens_test.go
│ │ ├── tokens.go
│ │ ├── tools_params_test.go
│ │ ├── tools_params.go
│ │ ├── tools_test.go
│ │ └── tools.go
│ └── toolsets
│ ├── toolsets_test.go
│ └── toolsets.go
├── README.md
└── SECURITY.md
```
# Files
--------------------------------------------------------------------------------
/pkg/mcpgo/tool_test.go:
--------------------------------------------------------------------------------
```go
package mcpgo
import (
"context"
"encoding/json"
"testing"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
)
func TestNewTool(t *testing.T) {
t.Run("creates tool with all fields", func(t *testing.T) {
handler := func(
ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
}
tool := NewTool(
"test-tool",
"Test description",
[]ToolParameter{WithString("param1")},
handler,
)
assert.NotNil(t, tool)
assert.NotNil(t, tool.GetHandler())
})
t.Run("creates tool with empty parameters", func(t *testing.T) {
handler := func(
ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
}
tool := NewTool("test-tool", "Test", []ToolParameter{}, handler)
assert.NotNil(t, tool)
})
}
func TestMark3labsToolImpl_GetHandler(t *testing.T) {
t.Run("returns handler", func(t *testing.T) {
handler := func(
ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
}
tool := NewTool("test-tool", "Test", []ToolParameter{}, handler)
assert.NotNil(t, tool.GetHandler())
})
}
func TestMark3labsToolImpl_ToMCPServerTool(t *testing.T) {
t.Run("converts tool with string parameter", func(t *testing.T) {
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{WithString("param1")},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
assert.NotNil(t, mcpTool.Handler)
})
t.Run("converts tool with number parameter", func(t *testing.T) {
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{WithNumber("param1")},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
})
t.Run("converts tool with boolean parameter", func(t *testing.T) {
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{WithBoolean("param1")},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
})
t.Run("converts tool with object parameter", func(t *testing.T) {
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{WithObject("param1")},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
})
t.Run("converts tool with array parameter", func(t *testing.T) {
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{WithArray("param1")},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
})
t.Run("converts tool with integer parameter", func(t *testing.T) {
param := ToolParameter{
Name: "param1",
Schema: map[string]interface{}{"type": "integer"},
}
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{param},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
})
t.Run("converts tool with unknown type parameter", func(t *testing.T) {
param := ToolParameter{
Name: "param1",
Schema: map[string]interface{}{"type": "unknown"},
}
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{param},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
})
t.Run("converts tool with missing type parameter", func(t *testing.T) {
param := ToolParameter{
Name: "param1",
Schema: map[string]interface{}{},
}
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{param},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
})
t.Run("converts tool with non-string type", func(t *testing.T) {
param := ToolParameter{
Name: "param1",
Schema: map[string]interface{}{"type": 123},
}
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{param},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultText("success"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Tool)
})
t.Run("handler returns error result", func(t *testing.T) {
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return NewToolResultError("error occurred"), nil
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Handler)
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "test-tool",
Arguments: map[string]interface{}{},
},
}
result, err := mcpTool.Handler(context.Background(), req)
assert.NoError(t, err)
assert.NotNil(t, result)
})
t.Run("handler returns handler error", func(t *testing.T) {
tool := NewTool(
"test-tool",
"Test",
[]ToolParameter{},
func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
return nil, assert.AnError
},
)
mcpTool := tool.toMCPServerTool()
assert.NotNil(t, mcpTool.Handler)
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "test-tool",
Arguments: map[string]interface{}{},
},
}
result, err := mcpTool.Handler(context.Background(), req)
assert.Error(t, err)
assert.Nil(t, result)
})
}
func TestPropertyOption_Min(t *testing.T) {
t.Run("sets minimum for number", func(t *testing.T) {
schema := map[string]interface{}{"type": "number"}
Min(10.0)(schema)
assert.Equal(t, 10.0, schema["minimum"])
})
t.Run("sets minimum for integer", func(t *testing.T) {
schema := map[string]interface{}{"type": "integer"}
Min(5.0)(schema)
assert.Equal(t, 5.0, schema["minimum"])
})
t.Run("sets minLength for string", func(t *testing.T) {
schema := map[string]interface{}{"type": "string"}
Min(3.0)(schema)
assert.Equal(t, 3, schema["minLength"])
})
t.Run("sets minItems for array", func(t *testing.T) {
schema := map[string]interface{}{"type": "array"}
Min(2.0)(schema)
assert.Equal(t, 2, schema["minItems"])
})
t.Run("ignores for unknown type", func(t *testing.T) {
schema := map[string]interface{}{"type": "boolean"}
Min(1.0)(schema)
assert.NotContains(t, schema, "minimum")
assert.NotContains(t, schema, "minLength")
assert.NotContains(t, schema, "minItems")
})
t.Run("ignores for missing type", func(t *testing.T) {
schema := map[string]interface{}{}
Min(1.0)(schema)
assert.NotContains(t, schema, "minimum")
})
t.Run("ignores for non-string type", func(t *testing.T) {
schema := map[string]interface{}{"type": 123}
Min(1.0)(schema)
assert.NotContains(t, schema, "minimum")
})
}
func TestPropertyOption_Max(t *testing.T) {
t.Run("sets maximum for number", func(t *testing.T) {
schema := map[string]interface{}{"type": "number"}
Max(100.0)(schema)
assert.Equal(t, 100.0, schema["maximum"])
})
t.Run("sets maximum for integer", func(t *testing.T) {
schema := map[string]interface{}{"type": "integer"}
Max(50.0)(schema)
assert.Equal(t, 50.0, schema["maximum"])
})
t.Run("sets maxLength for string", func(t *testing.T) {
schema := map[string]interface{}{"type": "string"}
Max(10.0)(schema)
assert.Equal(t, 10, schema["maxLength"])
})
t.Run("sets maxItems for array", func(t *testing.T) {
schema := map[string]interface{}{"type": "array"}
Max(5.0)(schema)
assert.Equal(t, 5, schema["maxItems"])
})
t.Run("ignores for unknown type", func(t *testing.T) {
schema := map[string]interface{}{"type": "boolean"}
Max(1.0)(schema)
assert.NotContains(t, schema, "maximum")
})
t.Run("ignores for missing type", func(t *testing.T) {
schema := map[string]interface{}{}
Max(1.0)(schema)
assert.NotContains(t, schema, "maximum")
})
t.Run("ignores for non-string type value", func(t *testing.T) {
schema := map[string]interface{}{"type": 123}
Max(1.0)(schema)
assert.NotContains(t, schema, "maximum")
})
}
func TestPropertyOption_Pattern(t *testing.T) {
t.Run("sets pattern for string", func(t *testing.T) {
schema := map[string]interface{}{"type": "string"}
Pattern("^[a-z]+$")(schema)
assert.Equal(t, "^[a-z]+$", schema["pattern"])
})
t.Run("ignores for non-string type", func(t *testing.T) {
schema := map[string]interface{}{"type": "number"}
Pattern("^[a-z]+$")(schema)
assert.NotContains(t, schema, "pattern")
})
t.Run("ignores for missing type", func(t *testing.T) {
schema := map[string]interface{}{}
Pattern("^[a-z]+$")(schema)
assert.NotContains(t, schema, "pattern")
})
t.Run("ignores for non-string type value", func(t *testing.T) {
schema := map[string]interface{}{"type": 123}
Pattern("^[a-z]+$")(schema)
assert.NotContains(t, schema, "pattern")
})
}
func TestPropertyOption_Enum(t *testing.T) {
t.Run("sets enum values", func(t *testing.T) {
schema := map[string]interface{}{}
Enum("value1", "value2", "value3")(schema)
assert.Equal(t, []interface{}{"value1", "value2", "value3"}, schema["enum"])
})
t.Run("sets enum with mixed types", func(t *testing.T) {
schema := map[string]interface{}{}
Enum("value1", 123, true)(schema)
assert.Equal(t, []interface{}{"value1", 123, true}, schema["enum"])
})
}
func TestPropertyOption_DefaultValue(t *testing.T) {
t.Run("sets default string value", func(t *testing.T) {
schema := map[string]interface{}{}
DefaultValue("default")(schema)
assert.Equal(t, "default", schema["default"])
})
t.Run("sets default number value", func(t *testing.T) {
schema := map[string]interface{}{}
DefaultValue(42.0)(schema)
assert.Equal(t, 42.0, schema["default"])
})
t.Run("sets default boolean value", func(t *testing.T) {
schema := map[string]interface{}{}
DefaultValue(true)(schema)
assert.Equal(t, true, schema["default"])
})
}
func TestPropertyOption_MaxProperties(t *testing.T) {
t.Run("sets maxProperties for object", func(t *testing.T) {
schema := map[string]interface{}{"type": "object"}
MaxProperties(5)(schema)
assert.Equal(t, 5, schema["maxProperties"])
})
t.Run("ignores for non-object type", func(t *testing.T) {
schema := map[string]interface{}{"type": "string"}
MaxProperties(5)(schema)
assert.NotContains(t, schema, "maxProperties")
})
t.Run("ignores for missing type", func(t *testing.T) {
schema := map[string]interface{}{}
MaxProperties(5)(schema)
assert.NotContains(t, schema, "maxProperties")
})
}
func TestPropertyOption_MinProperties(t *testing.T) {
t.Run("sets minProperties for object", func(t *testing.T) {
schema := map[string]interface{}{"type": "object"}
MinProperties(2)(schema)
assert.Equal(t, 2, schema["minProperties"])
})
t.Run("ignores for non-object type", func(t *testing.T) {
schema := map[string]interface{}{"type": "string"}
MinProperties(2)(schema)
assert.NotContains(t, schema, "minProperties")
})
}
func TestPropertyOption_Required(t *testing.T) {
t.Run("sets required flag", func(t *testing.T) {
schema := map[string]interface{}{}
Required()(schema)
assert.Equal(t, true, schema["required"])
})
}
func TestPropertyOption_Description(t *testing.T) {
t.Run("sets description", func(t *testing.T) {
schema := map[string]interface{}{}
Description("Test description")(schema)
assert.Equal(t, "Test description", schema["description"])
})
}
func TestToolParameter_ApplyPropertyOptions(t *testing.T) {
t.Run("applies single option", func(t *testing.T) {
param := ToolParameter{
Name: "test",
Schema: map[string]interface{}{"type": "string"},
}
param.applyPropertyOptions(Description("Test desc"))
assert.Equal(t, "Test desc", param.Schema["description"])
})
t.Run("applies multiple options", func(t *testing.T) {
param := ToolParameter{
Name: "test",
Schema: map[string]interface{}{"type": "string"},
}
param.applyPropertyOptions(
Description("Test desc"),
Required(),
Min(3.0),
)
assert.Equal(t, "Test desc", param.Schema["description"])
assert.Equal(t, true, param.Schema["required"])
assert.Equal(t, 3, param.Schema["minLength"])
})
t.Run("applies no options", func(t *testing.T) {
param := ToolParameter{
Name: "test",
Schema: map[string]interface{}{"type": "string"},
}
param.applyPropertyOptions()
assert.Equal(t, "string", param.Schema["type"])
})
}
func TestWithString(t *testing.T) {
t.Run("creates string parameter without options", func(t *testing.T) {
param := WithString("test")
assert.Equal(t, "test", param.Name)
assert.Equal(t, "string", param.Schema["type"])
})
t.Run("creates string parameter with options", func(t *testing.T) {
param := WithString("test", Description("Test"), Required(), Min(3.0))
assert.Equal(t, "test", param.Name)
assert.Equal(t, "string", param.Schema["type"])
assert.Equal(t, "Test", param.Schema["description"])
assert.Equal(t, true, param.Schema["required"])
assert.Equal(t, 3, param.Schema["minLength"])
})
}
func TestWithNumber(t *testing.T) {
t.Run("creates number parameter without options", func(t *testing.T) {
param := WithNumber("test")
assert.Equal(t, "test", param.Name)
assert.Equal(t, "number", param.Schema["type"])
})
t.Run("creates number parameter with options", func(t *testing.T) {
param := WithNumber("test", Min(1.0), Max(100.0))
assert.Equal(t, "test", param.Name)
assert.Equal(t, "number", param.Schema["type"])
assert.Equal(t, 1.0, param.Schema["minimum"])
assert.Equal(t, 100.0, param.Schema["maximum"])
})
}
func TestWithBoolean(t *testing.T) {
t.Run("creates boolean parameter", func(t *testing.T) {
param := WithBoolean("test")
assert.Equal(t, "test", param.Name)
assert.Equal(t, "boolean", param.Schema["type"])
})
}
func TestWithObject(t *testing.T) {
t.Run("creates object parameter", func(t *testing.T) {
param := WithObject("test")
assert.Equal(t, "test", param.Name)
assert.Equal(t, "object", param.Schema["type"])
})
t.Run("creates object parameter with options", func(t *testing.T) {
param := WithObject("test", MinProperties(1), MaxProperties(5))
assert.Equal(t, "test", param.Name)
assert.Equal(t, "object", param.Schema["type"])
assert.Equal(t, 1, param.Schema["minProperties"])
assert.Equal(t, 5, param.Schema["maxProperties"])
})
}
func TestWithArray(t *testing.T) {
t.Run("creates array parameter", func(t *testing.T) {
param := WithArray("test")
assert.Equal(t, "test", param.Name)
assert.Equal(t, "array", param.Schema["type"])
})
t.Run("creates array parameter with options", func(t *testing.T) {
param := WithArray("test", Min(1.0), Max(10.0))
assert.Equal(t, "test", param.Name)
assert.Equal(t, "array", param.Schema["type"])
assert.Equal(t, 1, param.Schema["minItems"])
assert.Equal(t, 10, param.Schema["maxItems"])
})
}
func TestAddNumberPropertyOptions(t *testing.T) {
t.Run("adds minimum", func(t *testing.T) {
schema := map[string]interface{}{"minimum": 10.0}
opts := addNumberPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds maximum", func(t *testing.T) {
schema := map[string]interface{}{"maximum": 100.0}
opts := addNumberPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds both minimum and maximum", func(t *testing.T) {
schema := map[string]interface{}{
"minimum": 10.0,
"maximum": 100.0,
}
opts := addNumberPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("handles non-float64 minimum", func(t *testing.T) {
schema := map[string]interface{}{"minimum": "not-a-number"}
opts := addNumberPropertyOptions(nil, schema)
assert.Nil(t, opts)
})
}
func TestAddStringPropertyOptions(t *testing.T) {
t.Run("adds minLength", func(t *testing.T) {
schema := map[string]interface{}{"minLength": 3}
opts := addStringPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds maxLength", func(t *testing.T) {
schema := map[string]interface{}{"maxLength": 10}
opts := addStringPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds pattern", func(t *testing.T) {
schema := map[string]interface{}{"pattern": "^[a-z]+$"}
opts := addStringPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds all string options", func(t *testing.T) {
schema := map[string]interface{}{
"minLength": 3,
"maxLength": 10,
"pattern": "^[a-z]+$",
}
opts := addStringPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
}
func TestAddDefaultValueOptions(t *testing.T) {
t.Run("adds string default", func(t *testing.T) {
opts := addDefaultValueOptions(nil, "default")
assert.NotNil(t, opts)
})
t.Run("adds float64 default", func(t *testing.T) {
opts := addDefaultValueOptions(nil, 42.0)
assert.NotNil(t, opts)
})
t.Run("adds bool default", func(t *testing.T) {
opts := addDefaultValueOptions(nil, true)
assert.NotNil(t, opts)
})
t.Run("ignores unknown type", func(t *testing.T) {
opts := addDefaultValueOptions(nil, []string{"test"})
assert.Nil(t, opts)
})
}
func TestAddEnumOptions(t *testing.T) {
t.Run("adds enum with string values", func(t *testing.T) {
enumValues := []interface{}{"value1", "value2", "value3"}
opts := addEnumOptions(nil, enumValues)
assert.NotNil(t, opts)
})
t.Run("adds enum with mixed values", func(t *testing.T) {
enumValues := []interface{}{"value1", 123, "value2"}
opts := addEnumOptions(nil, enumValues)
assert.NotNil(t, opts)
})
t.Run("handles non-array enum", func(t *testing.T) {
opts := addEnumOptions(nil, "not-an-array")
assert.Nil(t, opts)
})
t.Run("handles empty enum array", func(t *testing.T) {
enumValues := []interface{}{123, 456} // Non-string values
opts := addEnumOptions(nil, enumValues)
assert.Nil(t, opts) // Should return nil since no string values
})
}
func TestAddObjectPropertyOptions(t *testing.T) {
t.Run("adds maxProperties", func(t *testing.T) {
schema := map[string]interface{}{"maxProperties": 5}
opts := addObjectPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds minProperties", func(t *testing.T) {
schema := map[string]interface{}{"minProperties": 2}
opts := addObjectPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds both properties", func(t *testing.T) {
schema := map[string]interface{}{
"minProperties": 1,
"maxProperties": 5,
}
opts := addObjectPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
}
func TestAddArrayPropertyOptions(t *testing.T) {
t.Run("adds minItems", func(t *testing.T) {
schema := map[string]interface{}{"minItems": 1}
opts := addArrayPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds maxItems", func(t *testing.T) {
schema := map[string]interface{}{"maxItems": 10}
opts := addArrayPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
t.Run("adds both items", func(t *testing.T) {
schema := map[string]interface{}{
"minItems": 1,
"maxItems": 10,
}
opts := addArrayPropertyOptions(nil, schema)
assert.NotNil(t, opts)
})
}
func TestConvertSchemaToPropertyOptions(t *testing.T) {
t.Run("converts complete schema", func(t *testing.T) {
schema := map[string]interface{}{
"type": "string",
"description": "Test param",
"required": true,
"minLength": 3,
"maxLength": 10,
"pattern": "^[a-z]+$",
"default": "default",
}
opts := convertSchemaToPropertyOptions(schema)
assert.NotNil(t, opts)
})
t.Run("converts number schema", func(t *testing.T) {
schema := map[string]interface{}{
"type": "number",
"minimum": 1.0,
"maximum": 100.0,
"default": 42.0,
}
opts := convertSchemaToPropertyOptions(schema)
assert.NotNil(t, opts)
})
t.Run("converts object schema", func(t *testing.T) {
schema := map[string]interface{}{
"type": "object",
"minProperties": 1,
"maxProperties": 5,
}
opts := convertSchemaToPropertyOptions(schema)
assert.NotNil(t, opts)
})
t.Run("converts array schema", func(t *testing.T) {
schema := map[string]interface{}{
"type": "array",
"minItems": 1,
"maxItems": 10,
}
opts := convertSchemaToPropertyOptions(schema)
assert.NotNil(t, opts)
})
t.Run("converts schema with enum", func(t *testing.T) {
schema := map[string]interface{}{
"type": "string",
"enum": []interface{}{"value1", "value2"},
}
opts := convertSchemaToPropertyOptions(schema)
assert.NotNil(t, opts)
})
t.Run("handles empty description", func(t *testing.T) {
schema := map[string]interface{}{
"type": "string",
"description": "",
}
opts := convertSchemaToPropertyOptions(schema)
// Empty description should not be added
// In Go, a nil slice is valid and has length 0
assert.Len(t, opts, 0)
})
t.Run("handles false required", func(t *testing.T) {
schema := map[string]interface{}{
"type": "string",
"required": false,
}
opts := convertSchemaToPropertyOptions(schema)
// False required should not be added
// In Go, a nil slice is valid and has length 0
assert.Len(t, opts, 0)
})
}
func TestNewToolResultJSON(t *testing.T) {
t.Run("creates JSON result from map", func(t *testing.T) {
data := map[string]interface{}{
"key": "value",
"num": 42,
}
result, err := NewToolResultJSON(data)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.False(t, result.IsError)
assert.NotEmpty(t, result.Text)
// Verify it's valid JSON
var decoded map[string]interface{}
err = json.Unmarshal([]byte(result.Text), &decoded)
assert.NoError(t, err)
assert.Equal(t, "value", decoded["key"])
})
t.Run("creates JSON result from struct", func(t *testing.T) {
type TestStruct struct {
Name string `json:"name"`
Age int `json:"age"`
}
data := TestStruct{Name: "Test", Age: 30}
result, err := NewToolResultJSON(data)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.False(t, result.IsError)
})
t.Run("creates JSON result from array", func(t *testing.T) {
data := []string{"item1", "item2"}
result, err := NewToolResultJSON(data)
assert.NoError(t, err)
assert.NotNil(t, result)
})
t.Run("handles unmarshalable data", func(t *testing.T) {
// Create a channel which cannot be marshaled to JSON
data := make(chan int)
result, err := NewToolResultJSON(data)
assert.Error(t, err)
assert.Nil(t, result)
})
}
func TestNewToolResultText(t *testing.T) {
t.Run("creates text result", func(t *testing.T) {
result := NewToolResultText("test text")
assert.NotNil(t, result)
assert.Equal(t, "test text", result.Text)
assert.False(t, result.IsError)
assert.Nil(t, result.Content)
})
t.Run("creates empty text result", func(t *testing.T) {
result := NewToolResultText("")
assert.NotNil(t, result)
assert.Equal(t, "", result.Text)
assert.False(t, result.IsError)
})
}
func TestNewToolResultError(t *testing.T) {
t.Run("creates error result", func(t *testing.T) {
result := NewToolResultError("error message")
assert.NotNil(t, result)
assert.Equal(t, "error message", result.Text)
assert.True(t, result.IsError)
assert.Nil(t, result.Content)
})
t.Run("creates empty error result", func(t *testing.T) {
result := NewToolResultError("")
assert.NotNil(t, result)
assert.Equal(t, "", result.Text)
assert.True(t, result.IsError)
})
}
```
--------------------------------------------------------------------------------
/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")
})
}
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/payments.go:
--------------------------------------------------------------------------------
```go
package razorpay
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"
rzpsdk "github.com/razorpay/razorpay-go"
"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
"github.com/razorpay/razorpay-mcp-server/pkg/observability"
)
// FetchPayment returns a tool that fetches payment details using payment_id
func FetchPayment(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_id",
mcpgo.Description("payment_id is unique identifier "+
"of the payment to be retrieved."),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "payment_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentId := params["payment_id"].(string)
payment, err := client.Payment.Fetch(paymentId, nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching payment failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(payment)
}
return mcpgo.NewTool(
"fetch_payment",
"Use this tool to retrieve the details of a specific payment "+
"using its id. Amount returned is in paisa",
parameters,
handler,
)
}
// FetchPaymentCardDetails returns a tool that fetches card details
// for a payment
func FetchPaymentCardDetails(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_id",
mcpgo.Description("Unique identifier of the payment for which "+
"you want to retrieve card details. Must start with 'pay_'"),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "payment_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentId := params["payment_id"].(string)
cardDetails, err := client.Payment.FetchCardDetails(
paymentId, nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching card details failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(cardDetails)
}
return mcpgo.NewTool(
"fetch_payment_card_details",
"Use this tool to retrieve the details of the card used to make a payment. "+
"Only works for payments made using a card.",
parameters,
handler,
)
}
// UpdatePayment returns a tool that updates the notes for a payment
func UpdatePayment(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_id",
mcpgo.Description("Unique identifier of the payment to be updated. "+
"Must start with 'pay_'"),
mcpgo.Required(),
),
mcpgo.WithObject(
"notes",
mcpgo.Description("Key-value pairs that can be used to store additional "+
"information about the payment. Values must be strings or integers."),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
paymentUpdateReq := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "payment_id").
ValidateAndAddRequiredMap(paymentUpdateReq, "notes")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentId := params["payment_id"].(string)
// Update the payment
updatedPayment, err := client.Payment.Edit(paymentId, paymentUpdateReq, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("updating payment failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(updatedPayment)
}
return mcpgo.NewTool(
"update_payment",
"Use this tool to update the notes field of a payment. Notes are "+
"key-value pairs that can be used to store additional information.", //nolint:lll
parameters,
handler,
)
}
// CapturePayment returns a tool that captures an authorized payment
func CapturePayment(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_id",
mcpgo.Description("Unique identifier of the payment to be captured. Should start with 'pay_'"), //nolint:lll
mcpgo.Required(),
),
mcpgo.WithNumber(
"amount",
mcpgo.Description("The amount to be captured (in paisa). "+
"Should be equal to the authorized amount"),
mcpgo.Required(),
),
mcpgo.WithString(
"currency",
mcpgo.Description("ISO code of the currency in which the payment "+
"was made (e.g., INR)"),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
paymentCaptureReq := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "payment_id").
ValidateAndAddRequiredInt(params, "amount").
ValidateAndAddRequiredString(paymentCaptureReq, "currency")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentId := params["payment_id"].(string)
amount := int(params["amount"].(int64))
// Capture the payment
payment, err := client.Payment.Capture(
paymentId,
amount,
paymentCaptureReq,
nil,
)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("capturing payment failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(payment)
}
return mcpgo.NewTool(
"capture_payment",
"Use this tool to capture a previously authorized payment. Only payments with 'authorized' status can be captured", //nolint:lll
parameters,
handler,
)
}
// FetchAllPayments returns a tool to fetch multiple payments with filtering and pagination
//
//nolint:lll
func FetchAllPayments(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
// Pagination parameters
mcpgo.WithNumber(
"count",
mcpgo.Description("Number of payments to fetch "+
"(default: 10, max: 100)"),
mcpgo.Min(1),
mcpgo.Max(100),
),
mcpgo.WithNumber(
"skip",
mcpgo.Description("Number of payments to skip (default: 0)"),
mcpgo.Min(0),
),
// Time range filters
mcpgo.WithNumber(
"from",
mcpgo.Description("Unix timestamp (in seconds) from when "+
"payments are to be fetched"),
mcpgo.Min(0),
),
mcpgo.WithNumber(
"to",
mcpgo.Description("Unix timestamp (in seconds) up till when "+
"payments are to be fetched"),
mcpgo.Min(0),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
// Create query parameters map
paymentListOptions := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddPagination(paymentListOptions).
ValidateAndAddOptionalInt(paymentListOptions, "from").
ValidateAndAddOptionalInt(paymentListOptions, "to")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Fetch all payments using Razorpay SDK
payments, err := client.Payment.All(paymentListOptions, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("fetching payments failed: %s", err.Error())), nil
}
return mcpgo.NewToolResultJSON(payments)
}
return mcpgo.NewTool(
"fetch_all_payments",
"Fetch all payments with optional filtering and pagination",
parameters,
handler,
)
}
// extractPaymentID extracts the payment ID from the payment response
func extractPaymentID(payment map[string]interface{}) string {
if id, exists := payment["razorpay_payment_id"]; exists && id != nil {
return id.(string)
}
return ""
}
// extractNextActions extracts all available actions from the payment response
func extractNextActions(
payment map[string]interface{},
) []map[string]interface{} {
var actions []map[string]interface{}
if nextArray, exists := payment["next"]; exists && nextArray != nil {
if nextSlice, ok := nextArray.([]interface{}); ok {
for _, item := range nextSlice {
if nextItem, ok := item.(map[string]interface{}); ok {
actions = append(actions, nextItem)
}
}
}
}
return actions
}
// OTPResponse represents the response from OTP generation API
// sendOtp sends an OTP to the customer and returns the response
func sendOtp(otpUrl string) error {
if otpUrl == "" {
return fmt.Errorf("OTP URL is empty")
}
// Validate URL is safe and from Razorpay domain for security
parsedURL, err := url.Parse(otpUrl)
if err != nil {
return fmt.Errorf("invalid OTP URL: %s", err.Error())
}
if parsedURL.Scheme != "https" {
return fmt.Errorf("OTP URL must use HTTPS")
}
if !strings.Contains(parsedURL.Host, "razorpay.com") {
return fmt.Errorf("OTP URL must be from Razorpay domain")
}
// Create a secure HTTP client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("POST", otpUrl, nil)
if err != nil {
return fmt.Errorf("failed to create OTP request: %s", err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("OTP generation failed: %s", err.Error())
}
defer resp.Body.Close()
// Validate HTTP response status
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("OTP generation failed with HTTP status: %d",
resp.StatusCode)
}
return nil
}
// buildInitiatePaymentResponse constructs the response for initiate payment
func buildInitiatePaymentResponse(
payment map[string]interface{},
paymentID string,
actions []map[string]interface{},
) (map[string]interface{}, string) {
response := map[string]interface{}{
"razorpay_payment_id": paymentID,
"payment_details": payment,
"status": "payment_initiated",
"message": "Payment initiated successfully using " +
"S2S JSON v1 flow",
}
otpUrl := ""
if len(actions) > 0 {
response["available_actions"] = actions
// Add guidance based on available actions
var actionTypes []string
hasOTP := false
hasRedirect := false
hasUPICollect := false
hasUPIIntent := false
for _, action := range actions {
if actionType, exists := action["action"]; exists {
actionStr := actionType.(string)
actionTypes = append(actionTypes, actionStr)
if actionStr == "otp_generate" {
hasOTP = true
otpUrl = action["url"].(string)
}
if actionStr == "redirect" {
hasRedirect = true
}
if actionStr == "upi_collect" {
hasUPICollect = true
}
if actionStr == "upi_intent" {
hasUPIIntent = true
}
}
}
switch {
case hasOTP:
response["message"] = "Payment initiated. OTP authentication is " +
"available. " +
"Use the 'submit_otp' tool to submit OTP received by the customer " +
"for authentication."
addNextStepInstructions(response, paymentID)
case hasRedirect:
response["message"] = "Payment initiated. Redirect authentication is " +
"available. Use the redirect URL provided in available_actions."
case hasUPICollect:
response["message"] = fmt.Sprintf(
"Payment initiated. Available actions: %v", actionTypes)
case hasUPIIntent:
response["message"] = fmt.Sprintf(
"Payment initiated. Available actions: %v", actionTypes)
default:
response["message"] = fmt.Sprintf(
"Payment initiated. Available actions: %v", actionTypes)
}
} else {
addFallbackNextStepInstructions(response, paymentID)
}
return response, otpUrl
}
// addNextStepInstructions adds next step guidance to the response
func addNextStepInstructions(
response map[string]interface{},
paymentID string,
) {
if paymentID != "" {
response["next_step"] = "Use 'resend_otp' to regenerate OTP or " +
"'submit_otp' to proceed to enter OTP."
response["next_tool"] = "resend_otp"
response["next_tool_params"] = map[string]interface{}{
"payment_id": paymentID,
}
}
}
// addFallbackNextStepInstructions adds fallback next step guidance
func addFallbackNextStepInstructions(
response map[string]interface{},
paymentID string,
) {
if paymentID != "" {
response["next_step"] = "Use 'resend_otp' to regenerate OTP or " +
"'submit_otp' to proceed to enter OTP if " +
"OTP authentication is required."
response["next_tool"] = "resend_otp"
response["next_tool_params"] = map[string]interface{}{
"payment_id": paymentID,
}
}
}
// addContactAndEmailToPaymentData adds contact and email to payment data
func addContactAndEmailToPaymentData(
paymentData map[string]interface{},
params map[string]interface{},
) {
// Add contact if provided
if contact, exists := params["contact"]; exists && contact != "" {
paymentData["contact"] = contact
}
// Add email if provided, otherwise generate from contact
if email, exists := params["email"]; exists && email != "" {
paymentData["email"] = email
} else if contact, exists := paymentData["contact"]; exists && contact != "" {
paymentData["email"] = contact.(string) + "@mcp.razorpay.com"
}
}
// addAdditionalPaymentParameters adds additional parameters for UPI collect
// and other flows
func addAdditionalPaymentParameters(
paymentData map[string]interface{},
params map[string]interface{},
) {
// Note: customer_id is now handled explicitly in buildPaymentData
// Add method if provided
if method, exists := params["method"]; exists && method != "" {
paymentData["method"] = method
}
// Add save if provided
if save, exists := params["save"]; exists {
paymentData["save"] = save
}
// Add recurring if provided
if recurring, exists := params["recurring"]; exists {
paymentData["recurring"] = recurring
}
// Add UPI parameters if provided
if upiParams, exists := params["upi"]; exists && upiParams != nil {
if upiMap, ok := upiParams.(map[string]interface{}); ok {
paymentData["upi"] = upiMap
}
}
}
// processUPIParameters handles VPA and UPI intent parameter processing
func processUPIParameters(params map[string]interface{}) {
vpa, hasVPA := params["vpa"]
upiIntent, hasUPIIntent := params["upi_intent"]
// Handle VPA parameter (UPI collect flow)
if hasVPA && vpa != "" {
// Set method to UPI
params["method"] = "upi"
// Set UPI parameters for collect flow
params["upi"] = map[string]interface{}{
"flow": "collect",
"expiry_time": "6",
"vpa": vpa,
}
}
// Handle UPI intent parameter (UPI intent flow)
if hasUPIIntent && upiIntent == true {
// Set method to UPI
params["method"] = "upi"
// Set UPI parameters for intent flow
params["upi"] = map[string]interface{}{
"flow": "intent",
}
}
}
// createOrGetCustomer creates or gets a customer if contact is provided
func createOrGetCustomer(
client *rzpsdk.Client,
params map[string]interface{},
) (map[string]interface{}, error) {
contactValue, exists := params["contact"]
if !exists || contactValue == "" {
return nil, nil
}
contact := contactValue.(string)
customerData := map[string]interface{}{
"contact": contact,
"fail_existing": "0", // Get existing customer if exists
}
// Create/get customer using Razorpay SDK
customer, err := client.Customer.Create(customerData, nil)
if err != nil {
return nil, fmt.Errorf(
"failed to create/fetch customer with contact %s: %v",
contact,
err,
)
}
return customer, nil
}
// buildPaymentData constructs the payment data for the API call
func buildPaymentData(
params map[string]interface{},
currency string,
customerId string,
) *map[string]interface{} {
paymentData := map[string]interface{}{
"amount": params["amount"],
"currency": currency,
"order_id": params["order_id"],
}
if customerId != "" {
paymentData["customer_id"] = customerId
}
// Add token if provided (required for saved payment methods,
// optional for UPI collect)
if token, exists := params["token"]; exists && token != "" {
paymentData["token"] = token
}
// Add contact and email parameters
addContactAndEmailToPaymentData(paymentData, params)
// Add additional parameters for UPI collect and other flows
addAdditionalPaymentParameters(paymentData, params)
// Add force_terminal_id if provided (for single block multiple debit orders)
if terminalID, exists := params["force_terminal_id"]; exists &&
terminalID != "" {
paymentData["force_terminal_id"] = terminalID
}
return &paymentData
}
// processPaymentResult processes the payment creation result
func processPaymentResult(
payment map[string]interface{},
) (map[string]interface{}, error) {
// Extract payment ID and next actions from the response
paymentID := extractPaymentID(payment)
actions := extractNextActions(payment)
// Build structured response using the helper function
response, otpUrl := buildInitiatePaymentResponse(payment, paymentID, actions)
// Only send OTP if there's an OTP URL
if otpUrl != "" {
err := sendOtp(otpUrl)
if err != nil {
return nil, fmt.Errorf("OTP generation failed: %s", err.Error())
}
}
return response, nil
}
// InitiatePayment returns a tool that initiates a payment using order_id
// and token
// This implements the S2S JSON v1 flow for creating payments
func InitiatePayment(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithNumber(
"amount",
mcpgo.Description("Payment amount in the smallest currency sub-unit "+
"(e.g., for ₹100, use 10000)"),
mcpgo.Required(),
mcpgo.Min(100),
),
mcpgo.WithString(
"currency",
mcpgo.Description("Currency code for the payment. Default is 'INR'"),
),
mcpgo.WithString(
"token",
mcpgo.Description("Token ID of the saved payment method. "+
"Must start with 'token_'"),
),
mcpgo.WithString(
"order_id",
mcpgo.Description("Order ID for which the payment is being initiated. "+
"Must start with 'order_'"),
mcpgo.Required(),
),
mcpgo.WithString(
"email",
mcpgo.Description("Customer's email address (optional)"),
),
mcpgo.WithString(
"contact",
mcpgo.Description("Customer's phone number"),
),
mcpgo.WithString(
"customer_id",
mcpgo.Description("Customer ID for the payment. "+
"Must start with 'cust_'"),
),
mcpgo.WithBoolean(
"save",
mcpgo.Description("Whether to save the payment method for future use"),
),
mcpgo.WithString(
"vpa",
mcpgo.Description("Virtual Payment Address (VPA) for UPI payment. "+
"When provided, automatically sets method='upi' and UPI parameters "+
"with flow='collect' and expiry_time='6' (e.g., '9876543210@ptsbi')"),
),
mcpgo.WithBoolean(
"upi_intent",
mcpgo.Description("Enable UPI intent flow. "+
"When set to true, automatically sets method='upi' and UPI parameters "+
"with flow='intent'. The API will return a UPI URL in the response."),
),
mcpgo.WithBoolean(
"recurring",
mcpgo.Description("Set this to true for recurring payments like "+
"single block multiple debit."),
),
mcpgo.WithString(
"force_terminal_id",
mcpgo.Description("Terminal ID to be passed in case of single block "+
"multiple debit order."),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredInt(params, "amount").
ValidateAndAddOptionalString(params, "currency").
ValidateAndAddOptionalString(params, "token").
ValidateAndAddRequiredString(params, "order_id").
ValidateAndAddOptionalString(params, "email").
ValidateAndAddOptionalString(params, "contact").
ValidateAndAddOptionalString(params, "customer_id").
ValidateAndAddOptionalBool(params, "save").
ValidateAndAddOptionalString(params, "vpa").
ValidateAndAddOptionalBool(params, "upi_intent").
ValidateAndAddOptionalBool(params, "recurring").
ValidateAndAddOptionalString(params, "force_terminal_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
// Set default currency
currency := "INR"
if c, exists := params["currency"]; exists && c != "" {
currency = c.(string)
}
// Process UPI parameters (VPA for collect flow, upi_intent for intent flow)
processUPIParameters(params)
// Handle customer ID
var customerID string
if custID, exists := params["customer_id"]; exists && custID != "" {
customerID = custID.(string)
} else {
// Create or get customer if contact is provided
customer, err := createOrGetCustomer(client, params)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
if customer != nil {
if id, ok := customer["id"].(string); ok {
customerID = id
}
}
}
// Build payment data
paymentDataPtr := buildPaymentData(params, currency, customerID)
paymentData := *paymentDataPtr
// Create payment using Razorpay SDK's CreatePaymentJson method
// This follows the S2S JSON v1 flow:
// https://api.razorpay.com/v1/payments/create/json
payment, err := client.Payment.CreatePaymentJson(paymentData, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("initiating payment failed: %s", err.Error())), nil
}
// Process payment result
response, err := processPaymentResult(payment)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
return mcpgo.NewToolResultJSON(response)
}
return mcpgo.NewTool(
"initiate_payment",
"Initiate a payment using the S2S JSON v1 flow. "+
"Required parameters: amount and order_id. "+
"For saved payment methods, provide token. "+
"For UPI collect flow, provide 'vpa' parameter "+
"which automatically sets UPI with flow='collect' and expiry_time='6'. "+
"For UPI intent flow, set 'upi_intent=true' parameter "+
"which automatically sets UPI with flow='intent' and API returns UPI URL. "+
"Supports additional parameters like customer_id, email, "+
"contact, save, and recurring. "+
"Returns payment details including next action steps if required.",
parameters,
handler,
)
}
// ResendOtp returns a tool that sends OTP for payment authentication
func ResendOtp(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"payment_id",
mcpgo.Description("Unique identifier of the payment for which "+
"OTP needs to be generated. Must start with 'pay_'"),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "payment_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentID := params["payment_id"].(string)
// Resend OTP using Razorpay SDK
otpResponse, err := client.Payment.OtpResend(paymentID, nil, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("OTP resend failed: %s", err.Error())), nil
}
// Extract OTP submit URL from response
otpSubmitURL := extractOtpSubmitURL(otpResponse)
// Prepare response
response := map[string]interface{}{
"payment_id": paymentID,
"status": "success",
"message": "OTP sent successfully. Please enter the OTP received on your " +
"mobile number to complete the payment.",
"response_data": otpResponse,
}
// Add next step instructions if OTP submit URL is available
if otpSubmitURL != "" {
response["otp_submit_url"] = otpSubmitURL
response["next_step"] = "Use 'submit_otp' tool with the OTP code received " +
"from user to complete payment authentication."
response["next_tool"] = "submit_otp"
response["next_tool_params"] = map[string]interface{}{
"payment_id": paymentID,
"otp_string": "{OTP_CODE_FROM_USER}",
}
} else {
response["next_step"] = "Use 'submit_otp' tool with the OTP code received " +
"from user to complete payment authentication."
response["next_tool"] = "submit_otp"
response["next_tool_params"] = map[string]interface{}{
"payment_id": paymentID,
"otp_string": "{OTP_CODE_FROM_USER}",
}
}
result, err := mcpgo.NewToolResultJSON(response)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("JSON marshal error: %v", err)), nil
}
return result, nil
}
return mcpgo.NewTool(
"resend_otp",
"Resend OTP to the customer's registered mobile number if the previous "+
"OTP was not received or has expired.",
parameters,
handler,
)
}
// SubmitOtp returns a tool that submits OTP for payment verification
func SubmitOtp(
obs *observability.Observability,
client *rzpsdk.Client,
) mcpgo.Tool {
parameters := []mcpgo.ToolParameter{
mcpgo.WithString(
"otp_string",
mcpgo.Description("OTP string received from the user"),
mcpgo.Required(),
),
mcpgo.WithString(
"payment_id",
mcpgo.Description("Unique identifier of the payment for which "+
"OTP needs to be submitted. Must start with 'pay_'"),
mcpgo.Required(),
),
}
handler := func(
ctx context.Context,
r mcpgo.CallToolRequest,
) (*mcpgo.ToolResult, error) {
// Get client from context or use default
client, err := getClientFromContextOrDefault(ctx, client)
if err != nil {
return mcpgo.NewToolResultError(err.Error()), nil
}
params := make(map[string]interface{})
validator := NewValidator(&r).
ValidateAndAddRequiredString(params, "otp_string").
ValidateAndAddRequiredString(params, "payment_id")
if result, err := validator.HandleErrorsIfAny(); result != nil {
return result, err
}
paymentID := params["payment_id"].(string)
data := map[string]interface{}{
"otp": params["otp_string"].(string),
}
otpResponse, err := client.Payment.OtpSubmit(paymentID, data, nil)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("OTP verification failed: %s", err.Error())), nil
}
// Prepare response
response := map[string]interface{}{
"payment_id": paymentID,
"status": "success",
"message": "OTP verified successfully.",
"response_data": otpResponse,
}
result, err := mcpgo.NewToolResultJSON(response)
if err != nil {
return mcpgo.NewToolResultError(
fmt.Sprintf("JSON marshal error: %v", err)), nil
}
return result, nil
}
return mcpgo.NewTool(
"submit_otp",
"Verify and submit the OTP received by the customer to complete "+
"the payment authentication process.",
parameters,
handler,
)
}
// extractOtpSubmitURL extracts the OTP submit URL from the payment response
func extractOtpSubmitURL(responseData interface{}) string {
jsonData, ok := responseData.(map[string]interface{})
if !ok {
return ""
}
nextArray, exists := jsonData["next"]
if !exists || nextArray == nil {
return ""
}
nextSlice, ok := nextArray.([]interface{})
if !ok {
return ""
}
for _, item := range nextSlice {
nextItem, ok := item.(map[string]interface{})
if !ok {
continue
}
action, exists := nextItem["action"]
if !exists || action != "otp_submit" {
continue
}
submitURL, exists := nextItem["url"]
if exists && submitURL != nil {
if urlStr, ok := submitURL.(string); ok {
return urlStr
}
}
}
return ""
}
```
--------------------------------------------------------------------------------
/pkg/razorpay/tools_params_test.go:
--------------------------------------------------------------------------------
```go
package razorpay
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
)
func TestValidator(t *testing.T) {
tests := []struct {
name string
args map[string]interface{}
paramName string
validationFunc func(*Validator, map[string]interface{}, string) *Validator
expectError bool
expectValue interface{}
expectKey string
}{
// String tests
{
name: "required string - valid",
args: map[string]interface{}{"test_param": "test_value"},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredString,
expectError: false,
expectValue: "test_value",
expectKey: "test_param",
},
{
name: "required string - missing",
args: map[string]interface{}{},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredString,
expectError: true,
expectValue: nil,
expectKey: "test_param",
},
{
name: "optional string - valid",
args: map[string]interface{}{"test_param": "test_value"},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalString,
expectError: false,
expectValue: "test_value",
expectKey: "test_param",
},
{
name: "optional string - empty",
args: map[string]interface{}{"test_param": ""},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalString,
expectError: false,
expectValue: "",
expectKey: "test_param",
},
// Int tests
{
name: "required int - valid",
args: map[string]interface{}{"test_param": float64(123)},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredInt,
expectError: false,
expectValue: int64(123),
expectKey: "test_param",
},
{
name: "optional int - valid",
args: map[string]interface{}{"test_param": float64(123)},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalInt,
expectError: false,
expectValue: int64(123),
expectKey: "test_param",
},
{
name: "optional int - zero",
args: map[string]interface{}{"test_param": float64(0)},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalInt,
expectError: false,
expectValue: int64(0), // we expect the zero values as is
expectKey: "test_param",
},
// Float tests
{
name: "required float - valid",
args: map[string]interface{}{"test_param": float64(123.45)},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredFloat,
expectError: false,
expectValue: float64(123.45),
expectKey: "test_param",
},
{
name: "optional float - valid",
args: map[string]interface{}{"test_param": float64(123.45)},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalFloat,
expectError: false,
expectValue: float64(123.45),
expectKey: "test_param",
},
{
name: "optional float - zero",
args: map[string]interface{}{"test_param": float64(0)},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalFloat,
expectError: false,
expectValue: float64(0),
expectKey: "test_param",
},
// Bool tests
{
name: "required bool - true",
args: map[string]interface{}{"test_param": true},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredBool,
expectError: false,
expectValue: true,
expectKey: "test_param",
},
{
name: "required bool - false",
args: map[string]interface{}{"test_param": false},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredBool,
expectError: false,
expectValue: false,
expectKey: "test_param",
},
{
name: "optional bool - true",
args: map[string]interface{}{"test_param": true},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalBool,
expectError: false,
expectValue: true,
expectKey: "test_param",
},
{
name: "optional bool - false",
args: map[string]interface{}{"test_param": false},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalBool,
expectError: false,
expectValue: false,
expectKey: "test_param",
},
// Map tests
{
name: "required map - valid",
args: map[string]interface{}{
"test_param": map[string]interface{}{"key": "value"},
},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredMap,
expectError: false,
expectValue: map[string]interface{}{"key": "value"},
expectKey: "test_param",
},
{
name: "optional map - valid",
args: map[string]interface{}{
"test_param": map[string]interface{}{"key": "value"},
},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalMap,
expectError: false,
expectValue: map[string]interface{}{"key": "value"},
expectKey: "test_param",
},
{
name: "optional map - empty",
args: map[string]interface{}{
"test_param": map[string]interface{}{},
},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalMap,
expectError: false,
expectValue: map[string]interface{}{},
expectKey: "test_param",
},
// Array tests
{
name: "required array - valid",
args: map[string]interface{}{
"test_param": []interface{}{"value1", "value2"},
},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredArray,
expectError: false,
expectValue: []interface{}{"value1", "value2"},
expectKey: "test_param",
},
{
name: "optional array - valid",
args: map[string]interface{}{
"test_param": []interface{}{"value1", "value2"},
},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalArray,
expectError: false,
expectValue: []interface{}{"value1", "value2"},
expectKey: "test_param",
},
{
name: "optional array - empty",
args: map[string]interface{}{"test_param": []interface{}{}},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddOptionalArray,
expectError: false,
expectValue: []interface{}{},
expectKey: "test_param",
},
// Invalid type tests
{
name: "required string - wrong type",
args: map[string]interface{}{"test_param": 123},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredString,
expectError: true,
expectValue: nil,
expectKey: "test_param",
},
{
name: "required int - wrong type",
args: map[string]interface{}{"test_param": "not a number"},
paramName: "test_param",
validationFunc: (*Validator).ValidateAndAddRequiredInt,
expectError: true,
expectValue: nil,
expectKey: "test_param",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := make(map[string]interface{})
request := &mcpgo.CallToolRequest{
Arguments: tt.args,
}
validator := NewValidator(request)
tt.validationFunc(validator, result, tt.paramName)
if tt.expectError {
assert.True(t, validator.HasErrors(), "Expected validation error")
} else {
assert.False(t, validator.HasErrors(), "Did not expect validation error")
assert.Equal(t,
tt.expectValue,
result[tt.expectKey],
"Parameter value mismatch",
)
}
})
}
}
func TestValidatorPagination(t *testing.T) {
tests := []struct {
name string
args map[string]interface{}
expectCount interface{}
expectSkip interface{}
expectError bool
}{
{
name: "valid pagination params",
args: map[string]interface{}{
"count": float64(10),
"skip": float64(5),
},
expectCount: int64(10),
expectSkip: int64(5),
expectError: false,
},
{
name: "zero pagination params",
args: map[string]interface{}{"count": float64(0), "skip": float64(0)},
expectCount: int64(0),
expectSkip: int64(0),
expectError: false,
},
{
name: "invalid count type",
args: map[string]interface{}{
"count": "not a number",
"skip": float64(5),
},
expectCount: nil,
expectSkip: int64(5),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := make(map[string]interface{})
request := &mcpgo.CallToolRequest{
Arguments: tt.args,
}
validator := NewValidator(request)
validator.ValidateAndAddPagination(result)
if tt.expectError {
assert.True(t, validator.HasErrors(), "Expected validation error")
} else {
assert.False(t, validator.HasErrors(), "Did not expect validation error")
}
if tt.expectCount != nil {
assert.Equal(t, tt.expectCount, result["count"], "Count mismatch")
} else {
_, exists := result["count"]
assert.False(t, exists, "Count should not be added")
}
if tt.expectSkip != nil {
assert.Equal(t, tt.expectSkip, result["skip"], "Skip mismatch")
} else {
_, exists := result["skip"]
assert.False(t, exists, "Skip should not be added")
}
})
}
}
func TestValidatorExpand(t *testing.T) {
tests := []struct {
name string
args map[string]interface{}
expectExpand string
expectError bool
}{
{
name: "valid expand param",
args: map[string]interface{}{"expand": []interface{}{"payments"}},
expectExpand: "payments",
expectError: false,
},
{
name: "empty expand array",
args: map[string]interface{}{"expand": []interface{}{}},
expectExpand: "",
expectError: false,
},
{
name: "invalid expand type",
args: map[string]interface{}{"expand": "not an array"},
expectExpand: "",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := make(map[string]interface{})
request := &mcpgo.CallToolRequest{
Arguments: tt.args,
}
validator := NewValidator(request)
validator.ValidateAndAddExpand(result)
if tt.expectError {
assert.True(t, validator.HasErrors(), "Expected validation error")
} else {
assert.False(t, validator.HasErrors(), "Did not expect validation error")
if tt.expectExpand != "" {
assert.Equal(t,
tt.expectExpand,
result["expand[]"],
"Expand value mismatch",
)
} else {
_, exists := result["expand[]"]
assert.False(t, exists, "Expand should not be added")
}
}
})
}
}
// Test validator "To" functions which write to target maps
func TestValidatorToFunctions(t *testing.T) {
tests := []struct {
name string
args map[string]interface{}
paramName string
targetKey string
testFunc func(
*Validator, map[string]interface{}, string, string,
) *Validator
expectValue interface{}
expectError bool
}{
// ValidateAndAddOptionalStringToPath tests
{
name: "optional string to target - valid",
args: map[string]interface{}{"customer_name": "Test User"},
paramName: "customer_name",
targetKey: "name",
testFunc: (*Validator).ValidateAndAddOptionalStringToPath,
expectValue: "Test User",
expectError: false,
},
{
name: "optional string to target - empty",
args: map[string]interface{}{"customer_name": ""},
paramName: "customer_name",
targetKey: "name",
testFunc: (*Validator).ValidateAndAddOptionalStringToPath,
expectValue: "",
expectError: false,
},
{
name: "optional string to target - missing",
args: map[string]interface{}{},
paramName: "customer_name",
targetKey: "name",
testFunc: (*Validator).ValidateAndAddOptionalStringToPath,
expectValue: nil,
expectError: false,
},
{
name: "optional string to target - wrong type",
args: map[string]interface{}{"customer_name": 123},
paramName: "customer_name",
targetKey: "name",
testFunc: (*Validator).ValidateAndAddOptionalStringToPath,
expectValue: nil,
expectError: true,
},
// ValidateAndAddOptionalBoolToPath tests
{
name: "optional bool to target - true",
args: map[string]interface{}{"notify_sms": true},
paramName: "notify_sms",
targetKey: "sms",
testFunc: (*Validator).ValidateAndAddOptionalBoolToPath,
expectValue: true,
expectError: false,
},
{
name: "optional bool to target - false",
args: map[string]interface{}{"notify_sms": false},
paramName: "notify_sms",
targetKey: "sms",
testFunc: (*Validator).ValidateAndAddOptionalBoolToPath,
expectValue: false,
expectError: false,
},
{
name: "optional bool to target - wrong type",
args: map[string]interface{}{"notify_sms": "not a bool"},
paramName: "notify_sms",
targetKey: "sms",
testFunc: (*Validator).ValidateAndAddOptionalBoolToPath,
expectValue: nil,
expectError: true,
},
// ValidateAndAddOptionalIntToPath tests
{
name: "optional int to target - valid",
args: map[string]interface{}{"age": float64(25)},
paramName: "age",
targetKey: "customer_age",
testFunc: (*Validator).ValidateAndAddOptionalIntToPath,
expectValue: int64(25),
expectError: false,
},
{
name: "optional int to target - zero",
args: map[string]interface{}{"age": float64(0)},
paramName: "age",
targetKey: "customer_age",
testFunc: (*Validator).ValidateAndAddOptionalIntToPath,
expectValue: int64(0),
expectError: false,
},
{
name: "optional int to target - missing",
args: map[string]interface{}{},
paramName: "age",
targetKey: "customer_age",
testFunc: (*Validator).ValidateAndAddOptionalIntToPath,
expectValue: nil,
expectError: false,
},
{
name: "optional int to target - wrong type",
args: map[string]interface{}{"age": "not a number"},
paramName: "age",
targetKey: "customer_age",
testFunc: (*Validator).ValidateAndAddOptionalIntToPath,
expectValue: nil,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a target map for this specific test
target := make(map[string]interface{})
// Create the request and validator
request := &mcpgo.CallToolRequest{
Arguments: tt.args,
}
validator := NewValidator(request)
// Call the test function with target and verify its return value
tt.testFunc(validator, target, tt.paramName, tt.targetKey)
// Check if we got the expected errors
if tt.expectError {
assert.True(t, validator.HasErrors(), "Expected validation error")
} else {
assert.False(t, validator.HasErrors(), "Did not expect validation error")
// For non-error cases, check target map value
if tt.expectValue != nil {
// Should have the value with the target key
assert.Equal(t,
tt.expectValue,
target[tt.targetKey],
"Target map value mismatch")
} else {
// Target key should not exist
_, exists := target[tt.targetKey]
assert.False(t, exists, "Key should not be in target map when value is empty") // nolint:lll
}
}
})
}
}
// Test for nested validation with multiple fields into target maps
func TestValidatorNestedObjects(t *testing.T) {
t.Run("customer object validation", func(t *testing.T) {
// Create request with customer details
args := map[string]interface{}{
"customer_name": "John Doe",
"customer_email": "[email protected]",
"customer_contact": "+1234567890",
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
// Customer target map
customer := make(map[string]interface{})
// Create validator and validate customer fields
validator := NewValidator(request).
ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact")
// Should not have errors
assert.False(t, validator.HasErrors())
// Customer map should have all three fields
assert.Equal(t, "John Doe", customer["name"])
assert.Equal(t, "[email protected]", customer["email"])
assert.Equal(t, "+1234567890", customer["contact"])
})
t.Run("notification object validation", func(t *testing.T) {
// Create request with notification settings
args := map[string]interface{}{
"notify_sms": true,
"notify_email": false,
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
// Notify target map
notify := make(map[string]interface{})
// Create validator and validate notification fields
validator := NewValidator(request).
ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email")
// Should not have errors
assert.False(t, validator.HasErrors())
// Notify map should have both fields
assert.Equal(t, true, notify["sms"])
assert.Equal(t, false, notify["email"])
})
t.Run("mixed object with error", func(t *testing.T) {
// Create request with mixed valid and invalid data
args := map[string]interface{}{
"customer_name": "Jane Doe",
"customer_email": 12345, // Wrong type
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
// Target map
customer := make(map[string]interface{})
// Create validator and validate fields
validator := NewValidator(request).
ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
ValidateAndAddOptionalStringToPath(customer, "customer_email", "email")
// Should have errors
assert.True(t, validator.HasErrors())
// Customer map should have only the valid field
assert.Equal(t, "Jane Doe", customer["name"])
_, hasEmail := customer["email"]
assert.False(t, hasEmail, "Invalid field should not be added to target map")
})
}
// Test for optional bool handling
func TestOptionalBoolBehavior(t *testing.T) {
t.Run("explicit bool values", func(t *testing.T) {
// Create request with explicit bool values
args := map[string]interface{}{
"true_param": true,
"false_param": false,
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
// Create result map
result := make(map[string]interface{})
// Validate both parameters
validator := NewValidator(request).
ValidateAndAddOptionalBool(result, "true_param").
ValidateAndAddOptionalBool(result, "false_param")
// Verify no errors occurred
assert.False(t, validator.HasErrors())
// Both parameters should be set in the result
assert.Equal(t, true, result["true_param"])
assert.Equal(t, false, result["false_param"])
})
t.Run("missing bool parameter", func(t *testing.T) {
// Create request without bool parameters
args := map[string]interface{}{
"other_param": "some value",
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
// Create result map
result := make(map[string]interface{})
// Try to validate missing bool parameters
validator := NewValidator(request).
ValidateAndAddOptionalBool(result, "true_param").
ValidateAndAddOptionalBool(result, "false_param")
// Verify no errors occurred
assert.False(t, validator.HasErrors())
// Result should be empty since no bool values were provided
assert.Empty(t, result)
})
t.Run("explicit bool values with 'To' functions", func(t *testing.T) {
// Create request with explicit bool values
args := map[string]interface{}{
"notify_sms": true,
"notify_email": false,
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
// Create target map
target := make(map[string]interface{})
// Validate both parameters
validator := NewValidator(request).
ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms").
ValidateAndAddOptionalBoolToPath(target, "notify_email", "email")
// Verify no errors occurred
assert.False(t, validator.HasErrors())
// Both parameters should be set in the target map
assert.Equal(t, true, target["sms"])
assert.Equal(t, false, target["email"])
})
t.Run("missing bool parameter with 'To' functions", func(t *testing.T) {
// Create request without bool parameters
args := map[string]interface{}{
"other_param": "some value",
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
// Create target map
target := make(map[string]interface{})
// Try to validate missing bool parameters
validator := NewValidator(request).
ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms").
ValidateAndAddOptionalBoolToPath(target, "notify_email", "email")
// Verify no errors occurred
assert.False(t, validator.HasErrors())
// Target map should be empty since no bool values were provided
assert.Empty(t, target)
})
}
// Test for extractValueGeneric function edge cases
func TestExtractValueGeneric(t *testing.T) {
t.Run("invalid arguments type", func(t *testing.T) {
request := &mcpgo.CallToolRequest{
Arguments: "invalid_type", // Not a map
}
result, err := extractValueGeneric[string](request, "test", false)
assert.Error(t, err)
assert.Equal(t, "invalid arguments type", err.Error())
assert.Nil(t, result)
})
t.Run("json marshal error", func(t *testing.T) {
// Create a value that can't be marshaled to JSON
args := map[string]interface{}{
"test_param": make(chan int), // Channels can't be marshaled
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
result, err := extractValueGeneric[string](request, "test_param", false)
assert.Error(t, err)
assert.Equal(t, "invalid parameter type: test_param", err.Error())
assert.Nil(t, result)
})
t.Run("json unmarshal error", func(t *testing.T) {
// Provide a value that can't be unmarshaled to the target type
args := map[string]interface{}{
"test_param": []interface{}{1, 2, 3}, // Array can't be unmarshaled to string
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
result, err := extractValueGeneric[string](request, "test_param", false)
assert.Error(t, err)
assert.Equal(t, "invalid parameter type: test_param", err.Error())
assert.Nil(t, result)
})
}
// Test for validateAndAddRequired function
func TestValidateAndAddRequired(t *testing.T) {
t.Run("successful validation", func(t *testing.T) {
args := map[string]interface{}{
"test_param": "test_value",
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddRequired[string](validator, params, "test_param")
assert.False(t, result.HasErrors())
assert.Equal(t, "test_value", params["test_param"])
})
t.Run("validation error", func(t *testing.T) {
request := &mcpgo.CallToolRequest{
Arguments: "invalid_type",
}
params := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddRequired[string](validator, params, "test_param")
assert.True(t, result.HasErrors())
assert.Empty(t, params)
})
t.Run("nil value after successful extraction", func(t *testing.T) {
// This edge case is hard to trigger directly, but we can simulate it
// by using a type that extractValueGeneric might return as nil
args := map[string]interface{}{
"test_param": nil,
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddRequired[string](validator, params, "test_param")
// This should result in an error because the parameter is required
assert.True(t, result.HasErrors())
assert.Empty(t, params)
})
}
// Test for validateAndAddOptional function
func TestValidateAndAddOptional(t *testing.T) {
t.Run("successful validation", func(t *testing.T) {
args := map[string]interface{}{
"test_param": "test_value",
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddOptional[string](validator, params, "test_param")
assert.False(t, result.HasErrors())
assert.Equal(t, "test_value", params["test_param"])
})
t.Run("validation error", func(t *testing.T) {
request := &mcpgo.CallToolRequest{
Arguments: "invalid_type",
}
params := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddOptional[string](validator, params, "test_param")
assert.True(t, result.HasErrors())
assert.Empty(t, params)
})
t.Run("nil value handling", func(t *testing.T) {
args := map[string]interface{}{
"test_param": nil,
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddOptional[string](validator, params, "test_param")
assert.False(t, result.HasErrors())
assert.Empty(t, params)
})
}
// Test for validateAndAddToPath function
func TestValidateAndAddToPath(t *testing.T) {
t.Run("successful validation", func(t *testing.T) {
args := map[string]interface{}{
"test_param": "test_value",
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
target := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddToPath[string](
validator, target, "test_param", "target_key")
assert.False(t, result.HasErrors())
assert.Equal(t, "test_value", target["target_key"])
})
t.Run("validation error", func(t *testing.T) {
request := &mcpgo.CallToolRequest{
Arguments: "invalid_type",
}
target := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddToPath[string](
validator, target, "test_param", "target_key")
assert.True(t, result.HasErrors())
assert.Empty(t, target)
})
t.Run("nil value handling", func(t *testing.T) {
args := map[string]interface{}{
"test_param": nil,
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
target := make(map[string]interface{})
validator := NewValidator(request)
result := validateAndAddToPath[string](
validator, target, "test_param", "target_key")
assert.False(t, result.HasErrors())
assert.Empty(t, target)
})
}
// Test for ValidateAndAddPagination function
func TestValidateAndAddPagination(t *testing.T) {
t.Run("all pagination parameters", func(t *testing.T) {
args := map[string]interface{}{
"count": 10,
"skip": 5,
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddPagination(params)
assert.False(t, validator.HasErrors())
assert.Equal(t, int64(10), params["count"])
assert.Equal(t, int64(5), params["skip"])
})
t.Run("missing pagination parameters", func(t *testing.T) {
args := map[string]interface{}{}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddPagination(params)
assert.False(t, validator.HasErrors())
assert.Empty(t, params)
})
t.Run("invalid count type", func(t *testing.T) {
args := map[string]interface{}{
"count": "invalid",
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddPagination(params)
assert.True(t, validator.HasErrors())
})
}
// Test for ValidateAndAddExpand function
func TestValidateAndAddExpand(t *testing.T) {
t.Run("valid expand parameter", func(t *testing.T) {
args := map[string]interface{}{
"expand": []string{"payments", "customer"},
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddExpand(params)
assert.False(t, validator.HasErrors())
// The function sets expand[] for each value, so check the last one
assert.Equal(t, "customer", params["expand[]"])
})
t.Run("missing expand parameter", func(t *testing.T) {
args := map[string]interface{}{}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddExpand(params)
assert.False(t, validator.HasErrors())
assert.Empty(t, params)
})
t.Run("invalid expand type", func(t *testing.T) {
args := map[string]interface{}{
"expand": "invalid", // Should be []string, not string
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddExpand(params)
assert.True(t, validator.HasErrors())
})
}
// Test for token validation functions edge cases
func TestTokenValidationEdgeCases(t *testing.T) {
t.Run("validateTokenMaxAmount - int conversion", func(t *testing.T) {
token := map[string]interface{}{
"max_amount": 100, // int instead of float64
}
request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
validator := NewValidator(request).validateTokenMaxAmount(token)
assert.False(t, validator.HasErrors())
assert.Equal(t, float64(100), token["max_amount"])
})
t.Run("validateTokenExpireAt - int conversion", func(t *testing.T) {
token := map[string]interface{}{
"expire_at": 1234567890, // int instead of float64
}
request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
validator := NewValidator(request).validateTokenExpireAt(token)
assert.False(t, validator.HasErrors())
assert.Equal(t, float64(1234567890), token["expire_at"])
})
t.Run("validateTokenExpireAt - zero value", func(t *testing.T) {
token := map[string]interface{}{
"expire_at": 0,
}
request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
validator := NewValidator(request).validateTokenExpireAt(token)
assert.True(t, validator.HasErrors())
})
t.Run("validateTokenMaxAmount - zero value", func(t *testing.T) {
token := map[string]interface{}{
"max_amount": 0,
}
request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
validator := NewValidator(request).validateTokenMaxAmount(token)
assert.True(t, validator.HasErrors())
})
}
// Test for ValidateAndAddToken edge cases
func TestValidateAndAddTokenEdgeCases(t *testing.T) {
t.Run("token extraction error", func(t *testing.T) {
request := &mcpgo.CallToolRequest{
Arguments: "invalid_type",
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddToken(params, "token")
assert.True(t, validator.HasErrors())
assert.Empty(t, params)
})
t.Run("nil token value", func(t *testing.T) {
args := map[string]interface{}{
"token": nil,
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddToken(params, "token")
assert.False(t, validator.HasErrors())
assert.Empty(t, params)
})
t.Run("token validation errors", func(t *testing.T) {
args := map[string]interface{}{
"token": map[string]interface{}{
"max_amount": -100, // Invalid value
},
}
request := &mcpgo.CallToolRequest{
Arguments: args,
}
params := make(map[string]interface{})
validator := NewValidator(request).ValidateAndAddToken(params, "token")
assert.True(t, validator.HasErrors())
assert.Empty(t, params)
})
}
```