#
tokens: 34471/50000 3/69 files (page 4/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 5. Use http://codebase.md/razorpay/razorpay-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       └── new-tool-from-docs.mdc
├── .cursorignore
├── .dockerignore
├── .github
│   ├── CODEOWNERS
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   ├── pull_request_template.md
│   └── workflows
│       ├── assign.yml
│       ├── build.yml
│       ├── ci.yml
│       ├── docker-publish.yml
│       ├── lint.yml
│       └── release.yml
├── .gitignore
├── .golangci.yaml
├── .goreleaser.yaml
├── cmd
│   └── razorpay-mcp-server
│       ├── main_test.go
│       ├── main.go
│       ├── stdio_test.go
│       └── stdio.go
├── codecov.yml
├── CONTRIBUTING.md
├── coverage.out
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── pkg
│   ├── contextkey
│   │   ├── context_key_test.go
│   │   └── context_key.go
│   ├── log
│   │   ├── config_test.go
│   │   ├── config.go
│   │   ├── log.go
│   │   ├── slog_test.go
│   │   └── slog.go
│   ├── mcpgo
│   │   ├── README.md
│   │   ├── server_test.go
│   │   ├── server.go
│   │   ├── stdio_test.go
│   │   ├── stdio.go
│   │   ├── tool_test.go
│   │   ├── tool.go
│   │   └── transport.go
│   ├── observability
│   │   ├── observability_test.go
│   │   └── observability.go
│   ├── razorpay
│   │   ├── mock
│   │   │   ├── server_test.go
│   │   │   └── server.go
│   │   ├── orders_test.go
│   │   ├── orders.go
│   │   ├── payment_links_test.go
│   │   ├── payment_links.go
│   │   ├── payments_test.go
│   │   ├── payments.go
│   │   ├── payouts_test.go
│   │   ├── payouts.go
│   │   ├── qr_codes_test.go
│   │   ├── qr_codes.go
│   │   ├── README.md
│   │   ├── refunds_test.go
│   │   ├── refunds.go
│   │   ├── server_test.go
│   │   ├── server.go
│   │   ├── settlements_test.go
│   │   ├── settlements.go
│   │   ├── test_helpers.go
│   │   ├── tokens_test.go
│   │   ├── tokens.go
│   │   ├── tools_params_test.go
│   │   ├── tools_params.go
│   │   ├── tools_test.go
│   │   └── tools.go
│   └── toolsets
│       ├── toolsets_test.go
│       └── toolsets.go
├── README.md
└── SECURITY.md
```

# Files

--------------------------------------------------------------------------------
/pkg/razorpay/orders_test.go:
--------------------------------------------------------------------------------

```go
  1 | package razorpay
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"net/http"
  6 | 	"net/http/httptest"
  7 | 	"testing"
  8 | 
  9 | 	"github.com/razorpay/razorpay-go/constants"
 10 | 
 11 | 	"github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
 12 | )
 13 | 
 14 | func Test_CreateOrder(t *testing.T) {
 15 | 	createOrderPath := fmt.Sprintf(
 16 | 		"/%s%s",
 17 | 		constants.VERSION_V1,
 18 | 		constants.ORDER_URL,
 19 | 	)
 20 | 
 21 | 	// Define common response maps to be reused
 22 | 	orderWithAllParamsResp := map[string]interface{}{
 23 | 		"id":                       "order_EKwxwAgItmmXdp",
 24 | 		"amount":                   float64(10000),
 25 | 		"currency":                 "INR",
 26 | 		"receipt":                  "receipt-123",
 27 | 		"partial_payment":          true,
 28 | 		"first_payment_min_amount": float64(5000),
 29 | 		"notes": map[string]interface{}{
 30 | 			"customer_name": "test-customer",
 31 | 			"product_name":  "test-product",
 32 | 		},
 33 | 		"status": "created",
 34 | 	}
 35 | 
 36 | 	orderWithRequiredParamsResp := map[string]interface{}{
 37 | 		"id":       "order_EKwxwAgItmmXdp",
 38 | 		"amount":   float64(10000),
 39 | 		"currency": "INR",
 40 | 		"status":   "created",
 41 | 	}
 42 | 
 43 | 	errorResp := map[string]interface{}{
 44 | 		"error": map[string]interface{}{
 45 | 			"code":        "BAD_REQUEST_ERROR",
 46 | 			"description": "Razorpay API error: Bad request",
 47 | 		},
 48 | 	}
 49 | 
 50 | 	tests := []RazorpayToolTestCase{
 51 | 		{
 52 | 			Name: "successful order creation with all parameters",
 53 | 			Request: map[string]interface{}{
 54 | 				"amount":                   float64(10000),
 55 | 				"currency":                 "INR",
 56 | 				"receipt":                  "receipt-123",
 57 | 				"partial_payment":          true,
 58 | 				"first_payment_min_amount": float64(5000),
 59 | 				"notes": map[string]interface{}{
 60 | 					"customer_name": "test-customer",
 61 | 					"product_name":  "test-product",
 62 | 				},
 63 | 			},
 64 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 65 | 				return mock.NewHTTPClient(
 66 | 					mock.Endpoint{
 67 | 						Path:     createOrderPath,
 68 | 						Method:   "POST",
 69 | 						Response: orderWithAllParamsResp,
 70 | 					},
 71 | 				)
 72 | 			},
 73 | 			ExpectError:    false,
 74 | 			ExpectedResult: orderWithAllParamsResp,
 75 | 		},
 76 | 		{
 77 | 			Name: "successful order creation with required params only",
 78 | 			Request: map[string]interface{}{
 79 | 				"amount":   float64(10000),
 80 | 				"currency": "INR",
 81 | 			},
 82 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 83 | 				return mock.NewHTTPClient(
 84 | 					mock.Endpoint{
 85 | 						Path:     createOrderPath,
 86 | 						Method:   "POST",
 87 | 						Response: orderWithRequiredParamsResp,
 88 | 					},
 89 | 				)
 90 | 			},
 91 | 			ExpectError:    false,
 92 | 			ExpectedResult: orderWithRequiredParamsResp,
 93 | 		},
 94 | 		{
 95 | 			Name: "multiple validation errors",
 96 | 			Request: map[string]interface{}{
 97 | 				// Missing both amount and currency (required parameters)
 98 | 				"partial_payment":          "invalid_boolean", // Wrong type for boolean
 99 | 				"first_payment_min_amount": "invalid_number",  // Wrong type for number
100 | 			},
101 | 			MockHttpClient: nil, // No HTTP client needed for validation error
102 | 			ExpectError:    true,
103 | 			ExpectedErrMsg: "Validation errors:\n- " +
104 | 				"missing required parameter: amount\n- " +
105 | 				"missing required parameter: currency\n- " +
106 | 				"invalid parameter type: partial_payment",
107 | 		},
108 | 		{
109 | 			Name: "first_payment_min_amount validation when partial_payment is true",
110 | 			Request: map[string]interface{}{
111 | 				"amount":                   float64(10000),
112 | 				"currency":                 "INR",
113 | 				"partial_payment":          true,
114 | 				"first_payment_min_amount": "invalid_number",
115 | 			},
116 | 			MockHttpClient: nil, // No HTTP client needed for validation error
117 | 			ExpectError:    true,
118 | 			ExpectedErrMsg: "Validation errors:\n- " +
119 | 				"invalid parameter type: first_payment_min_amount",
120 | 		},
121 | 		{
122 | 			Name: "order creation fails",
123 | 			Request: map[string]interface{}{
124 | 				"amount":   float64(10000),
125 | 				"currency": "INR",
126 | 			},
127 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
128 | 				return mock.NewHTTPClient(
129 | 					mock.Endpoint{
130 | 						Path:     createOrderPath,
131 | 						Method:   "POST",
132 | 						Response: errorResp,
133 | 					},
134 | 				)
135 | 			},
136 | 			ExpectError:    true,
137 | 			ExpectedErrMsg: "creating order failed: Razorpay API error: Bad request",
138 | 		},
139 | 		{
140 | 			Name: "successful SBMD mandate order creation",
141 | 			Request: map[string]interface{}{
142 | 				"amount":      float64(500000),
143 | 				"currency":    "INR",
144 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
145 | 				"method":      "upi",
146 | 				"token": map[string]interface{}{
147 | 					"max_amount": float64(500000),
148 | 					"expire_at":  float64(2709971120),
149 | 					"frequency":  "as_presented",
150 | 					"type":       "single_block_multiple_debit",
151 | 				},
152 | 				"receipt": "Receipt No. 1",
153 | 				"notes": map[string]interface{}{
154 | 					"notes_key_1": "Tea, Earl Grey, Hot",
155 | 					"notes_key_2": "Tea, Earl Grey... decaf.",
156 | 				},
157 | 			},
158 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
159 | 				sbmdOrderResp := map[string]interface{}{
160 | 					"id":          "order_SBMD123456",
161 | 					"amount":      float64(500000),
162 | 					"currency":    "INR",
163 | 					"customer_id": "cust_4xbQrmEoA5WJ01",
164 | 					"method":      "upi",
165 | 					"token": map[string]interface{}{
166 | 						"max_amount": float64(500000),
167 | 						"expire_at":  float64(2709971120),
168 | 						"frequency":  "as_presented",
169 | 						"type":       "single_block_multiple_debit",
170 | 					},
171 | 					"receipt": "Receipt No. 1",
172 | 					"status":  "created",
173 | 					"notes": map[string]interface{}{
174 | 						"notes_key_1": "Tea, Earl Grey, Hot",
175 | 						"notes_key_2": "Tea, Earl Grey... decaf.",
176 | 					},
177 | 				}
178 | 				return mock.NewHTTPClient(
179 | 					mock.Endpoint{
180 | 						Path:     createOrderPath,
181 | 						Method:   "POST",
182 | 						Response: sbmdOrderResp,
183 | 					},
184 | 				)
185 | 			},
186 | 			ExpectError: false,
187 | 			ExpectedResult: map[string]interface{}{
188 | 				"id":          "order_SBMD123456",
189 | 				"amount":      float64(500000),
190 | 				"currency":    "INR",
191 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
192 | 				"method":      "upi",
193 | 				"token": map[string]interface{}{
194 | 					"max_amount": float64(500000),
195 | 					"expire_at":  float64(2709971120),
196 | 					"frequency":  "as_presented",
197 | 					"type":       "single_block_multiple_debit",
198 | 				},
199 | 				"receipt": "Receipt No. 1",
200 | 				"status":  "created",
201 | 				"notes": map[string]interface{}{
202 | 					"notes_key_1": "Tea, Earl Grey, Hot",
203 | 					"notes_key_2": "Tea, Earl Grey... decaf.",
204 | 				},
205 | 			},
206 | 		},
207 | 		{
208 | 			Name: "mandate order with invalid token parameter type",
209 | 			Request: map[string]interface{}{
210 | 				"amount":      float64(500000),
211 | 				"currency":    "INR",
212 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
213 | 				"method":      "upi",
214 | 				"token":       "invalid_token_should_be_object",
215 | 			},
216 | 			MockHttpClient: nil,
217 | 			ExpectError:    true,
218 | 			ExpectedErrMsg: "invalid parameter type: token",
219 | 		},
220 | 		{
221 | 			Name: "mandate order with invalid method parameter type",
222 | 			Request: map[string]interface{}{
223 | 				"amount":      float64(500000),
224 | 				"currency":    "INR",
225 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
226 | 				"method":      123,
227 | 				"token": map[string]interface{}{
228 | 					"max_amount": float64(500000),
229 | 					"expire_at":  float64(2709971120),
230 | 					"frequency":  "as_presented",
231 | 				},
232 | 			},
233 | 			MockHttpClient: nil,
234 | 			ExpectError:    true,
235 | 			ExpectedErrMsg: "invalid parameter type: method",
236 | 		},
237 | 		{
238 | 			Name: "token validation - missing max_amount",
239 | 			Request: map[string]interface{}{
240 | 				"amount":      float64(500000),
241 | 				"currency":    "INR",
242 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
243 | 				"method":      "upi",
244 | 				"token": map[string]interface{}{
245 | 					"expire_at": float64(2709971120),
246 | 					"frequency": "as_presented",
247 | 				},
248 | 			},
249 | 			MockHttpClient: nil,
250 | 			ExpectError:    true,
251 | 			ExpectedErrMsg: "token.max_amount is required",
252 | 		},
253 | 		{
254 | 			Name: "token validation - missing frequency",
255 | 			Request: map[string]interface{}{
256 | 				"amount":      float64(500000),
257 | 				"currency":    "INR",
258 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
259 | 				"method":      "upi",
260 | 				"token": map[string]interface{}{
261 | 					"max_amount": float64(500000),
262 | 					"expire_at":  float64(2709971120),
263 | 				},
264 | 			},
265 | 			MockHttpClient: nil,
266 | 			ExpectError:    true,
267 | 			ExpectedErrMsg: "token.frequency is required",
268 | 		},
269 | 		{
270 | 			Name: "token validation - invalid max_amount type",
271 | 			Request: map[string]interface{}{
272 | 				"amount":      float64(500000),
273 | 				"currency":    "INR",
274 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
275 | 				"method":      "upi",
276 | 				"token": map[string]interface{}{
277 | 					"max_amount": "invalid_string",
278 | 					"expire_at":  float64(2709971120),
279 | 					"frequency":  "as_presented",
280 | 				},
281 | 			},
282 | 			MockHttpClient: nil,
283 | 			ExpectError:    true,
284 | 			ExpectedErrMsg: "token.max_amount must be a number",
285 | 		},
286 | 		{
287 | 			Name: "token validation - invalid max_amount value",
288 | 			Request: map[string]interface{}{
289 | 				"amount":      float64(500000),
290 | 				"currency":    "INR",
291 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
292 | 				"method":      "upi",
293 | 				"token": map[string]interface{}{
294 | 					"max_amount": float64(-100),
295 | 					"expire_at":  float64(2709971120),
296 | 					"frequency":  "as_presented",
297 | 				},
298 | 			},
299 | 			MockHttpClient: nil,
300 | 			ExpectError:    true,
301 | 			ExpectedErrMsg: "token.max_amount must be greater than 0",
302 | 		},
303 | 		{
304 | 			Name: "token validation - invalid expire_at type",
305 | 			Request: map[string]interface{}{
306 | 				"amount":      float64(500000),
307 | 				"currency":    "INR",
308 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
309 | 				"method":      "upi",
310 | 				"token": map[string]interface{}{
311 | 					"max_amount": float64(500000),
312 | 					"expire_at":  "invalid_string",
313 | 					"frequency":  "as_presented",
314 | 				},
315 | 			},
316 | 			MockHttpClient: nil,
317 | 			ExpectError:    true,
318 | 			ExpectedErrMsg: "token.expire_at must be a number",
319 | 		},
320 | 		{
321 | 			Name: "token validation - invalid expire_at value",
322 | 			Request: map[string]interface{}{
323 | 				"amount":      float64(500000),
324 | 				"currency":    "INR",
325 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
326 | 				"method":      "upi",
327 | 				"token": map[string]interface{}{
328 | 					"max_amount": float64(500000),
329 | 					"expire_at":  float64(-100),
330 | 					"frequency":  "as_presented",
331 | 				},
332 | 			},
333 | 			MockHttpClient: nil,
334 | 			ExpectError:    true,
335 | 			ExpectedErrMsg: "token.expire_at must be greater than 0",
336 | 		},
337 | 		{
338 | 			Name: "token validation - invalid frequency type",
339 | 			Request: map[string]interface{}{
340 | 				"amount":      float64(500000),
341 | 				"currency":    "INR",
342 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
343 | 				"method":      "upi",
344 | 				"token": map[string]interface{}{
345 | 					"max_amount": float64(500000),
346 | 					"expire_at":  float64(2709971120),
347 | 					"frequency":  123,
348 | 				},
349 | 			},
350 | 			MockHttpClient: nil,
351 | 			ExpectError:    true,
352 | 			ExpectedErrMsg: "token.frequency must be a string",
353 | 		},
354 | 		{
355 | 			Name: "token validation - invalid frequency value",
356 | 			Request: map[string]interface{}{
357 | 				"amount":      float64(500000),
358 | 				"currency":    "INR",
359 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
360 | 				"method":      "upi",
361 | 				"token": map[string]interface{}{
362 | 					"max_amount": float64(500000),
363 | 					"expire_at":  float64(2709971120),
364 | 					"frequency":  "invalid_frequency",
365 | 				},
366 | 			},
367 | 			MockHttpClient: nil,
368 | 			ExpectError:    true,
369 | 			ExpectedErrMsg: "token.frequency must be one of: as_presented, " +
370 | 				"monthly, one_time, yearly, weekly, daily",
371 | 		},
372 | 		{
373 | 			Name: "token validation - invalid type value",
374 | 			Request: map[string]interface{}{
375 | 				"amount":      float64(500000),
376 | 				"currency":    "INR",
377 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
378 | 				"method":      "upi",
379 | 				"token": map[string]interface{}{
380 | 					"max_amount": float64(500000),
381 | 					"expire_at":  float64(2709971120),
382 | 					"frequency":  "as_presented",
383 | 					"type":       "invalid_type",
384 | 				},
385 | 			},
386 | 			MockHttpClient: nil,
387 | 			ExpectError:    true,
388 | 			ExpectedErrMsg: "token.type must be one of: single_block_multiple_debit",
389 | 		},
390 | 		{
391 | 			Name: "token validation - invalid type type",
392 | 			Request: map[string]interface{}{
393 | 				"amount":      float64(500000),
394 | 				"currency":    "INR",
395 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
396 | 				"method":      "upi",
397 | 				"token": map[string]interface{}{
398 | 					"max_amount": float64(500000),
399 | 					"expire_at":  float64(2709971120),
400 | 					"frequency":  "as_presented",
401 | 					"type":       123,
402 | 				},
403 | 			},
404 | 			MockHttpClient: nil,
405 | 			ExpectError:    true,
406 | 			ExpectedErrMsg: "token.type must be a string",
407 | 		},
408 | 		{
409 | 			Name: "token validation - missing type",
410 | 			Request: map[string]interface{}{
411 | 				"amount":      float64(500000),
412 | 				"currency":    "INR",
413 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
414 | 				"method":      "upi",
415 | 				"token": map[string]interface{}{
416 | 					"max_amount": float64(500000),
417 | 					"expire_at":  float64(2709971120),
418 | 					"frequency":  "as_presented",
419 | 				},
420 | 			},
421 | 			MockHttpClient: nil,
422 | 			ExpectError:    true,
423 | 			ExpectedErrMsg: "token.type is required",
424 | 		},
425 | 		{
426 | 			Name: "token validation - default expire_at when not provided",
427 | 			Request: map[string]interface{}{
428 | 				"amount":      float64(500000),
429 | 				"currency":    "INR",
430 | 				"customer_id": "cust_4xbQrmEoA5WJ01",
431 | 				"method":      "upi",
432 | 				"token": map[string]interface{}{
433 | 					"max_amount": float64(500000),
434 | 					"frequency":  "as_presented",
435 | 					"type":       "single_block_multiple_debit",
436 | 				},
437 | 			},
438 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
439 | 				return mock.NewHTTPClient(
440 | 					mock.Endpoint{
441 | 						Path:   createOrderPath,
442 | 						Method: "POST",
443 | 						Response: map[string]interface{}{
444 | 							"id": "order_test_12345",
445 | 						},
446 | 					},
447 | 				)
448 | 			},
449 | 			ExpectError: false,
450 | 			ExpectedResult: map[string]interface{}{
451 | 				"id": "order_test_12345",
452 | 			},
453 | 		},
454 | 	}
455 | 
456 | 	for _, tc := range tests {
457 | 		t.Run(tc.Name, func(t *testing.T) {
458 | 			runToolTest(t, tc, CreateOrder, "Order")
459 | 		})
460 | 	}
461 | }
462 | 
463 | func Test_FetchOrder(t *testing.T) {
464 | 	fetchOrderPathFmt := fmt.Sprintf(
465 | 		"/%s%s/%%s",
466 | 		constants.VERSION_V1,
467 | 		constants.ORDER_URL,
468 | 	)
469 | 
470 | 	orderResp := map[string]interface{}{
471 | 		"id":       "order_EKwxwAgItmmXdp",
472 | 		"amount":   float64(10000),
473 | 		"currency": "INR",
474 | 		"receipt":  "receipt-123",
475 | 		"status":   "created",
476 | 	}
477 | 
478 | 	orderNotFoundResp := map[string]interface{}{
479 | 		"error": map[string]interface{}{
480 | 			"code":        "BAD_REQUEST_ERROR",
481 | 			"description": "order not found",
482 | 		},
483 | 	}
484 | 
485 | 	tests := []RazorpayToolTestCase{
486 | 		{
487 | 			Name: "successful order fetch",
488 | 			Request: map[string]interface{}{
489 | 				"order_id": "order_EKwxwAgItmmXdp",
490 | 			},
491 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
492 | 				return mock.NewHTTPClient(
493 | 					mock.Endpoint{
494 | 						Path:     fmt.Sprintf(fetchOrderPathFmt, "order_EKwxwAgItmmXdp"),
495 | 						Method:   "GET",
496 | 						Response: orderResp,
497 | 					},
498 | 				)
499 | 			},
500 | 			ExpectError:    false,
501 | 			ExpectedResult: orderResp,
502 | 		},
503 | 		{
504 | 			Name: "order not found",
505 | 			Request: map[string]interface{}{
506 | 				"order_id": "order_invalid",
507 | 			},
508 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
509 | 				return mock.NewHTTPClient(
510 | 					mock.Endpoint{
511 | 						Path:     fmt.Sprintf(fetchOrderPathFmt, "order_invalid"),
512 | 						Method:   "GET",
513 | 						Response: orderNotFoundResp,
514 | 					},
515 | 				)
516 | 			},
517 | 			ExpectError:    true,
518 | 			ExpectedErrMsg: "fetching order failed: order not found",
519 | 		},
520 | 		{
521 | 			Name:           "missing order_id parameter",
522 | 			Request:        map[string]interface{}{},
523 | 			MockHttpClient: nil, // No HTTP client needed for validation error
524 | 			ExpectError:    true,
525 | 			ExpectedErrMsg: "missing required parameter: order_id",
526 | 		},
527 | 	}
528 | 
529 | 	for _, tc := range tests {
530 | 		t.Run(tc.Name, func(t *testing.T) {
531 | 			runToolTest(t, tc, FetchOrder, "Order")
532 | 		})
533 | 	}
534 | }
535 | 
536 | func Test_FetchAllOrders(t *testing.T) {
537 | 	fetchAllOrdersPath := fmt.Sprintf(
538 | 		"/%s%s",
539 | 		constants.VERSION_V1,
540 | 		constants.ORDER_URL,
541 | 	)
542 | 
543 | 	// Define the sample response for all orders
544 | 	ordersResp := map[string]interface{}{
545 | 		"entity": "collection",
546 | 		"count":  float64(2),
547 | 		"items": []interface{}{
548 | 			map[string]interface{}{
549 | 				"id":          "order_EKzX2WiEWbMxmx",
550 | 				"entity":      "order",
551 | 				"amount":      float64(1234),
552 | 				"amount_paid": float64(0),
553 | 				"amount_due":  float64(1234),
554 | 				"currency":    "INR",
555 | 				"receipt":     "Receipt No. 1",
556 | 				"offer_id":    nil,
557 | 				"status":      "created",
558 | 				"attempts":    float64(0),
559 | 				"notes":       []interface{}{},
560 | 				"created_at":  float64(1582637108),
561 | 			},
562 | 			map[string]interface{}{
563 | 				"id":          "order_EAI5nRfThga2TU",
564 | 				"entity":      "order",
565 | 				"amount":      float64(100),
566 | 				"amount_paid": float64(0),
567 | 				"amount_due":  float64(100),
568 | 				"currency":    "INR",
569 | 				"receipt":     "Receipt No. 1",
570 | 				"offer_id":    nil,
571 | 				"status":      "created",
572 | 				"attempts":    float64(0),
573 | 				"notes":       []interface{}{},
574 | 				"created_at":  float64(1580300731),
575 | 			},
576 | 		},
577 | 	}
578 | 
579 | 	// Define error response
580 | 	errorResp := map[string]interface{}{
581 | 		"error": map[string]interface{}{
582 | 			"code":        "BAD_REQUEST_ERROR",
583 | 			"description": "Razorpay API error: Bad request",
584 | 		},
585 | 	}
586 | 
587 | 	// Define the test cases
588 | 	tests := []RazorpayToolTestCase{
589 | 		{
590 | 			Name:    "successful fetch all orders with no parameters",
591 | 			Request: map[string]interface{}{},
592 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
593 | 				return mock.NewHTTPClient(
594 | 					mock.Endpoint{
595 | 						Path:     fetchAllOrdersPath,
596 | 						Method:   "GET",
597 | 						Response: ordersResp,
598 | 					},
599 | 				)
600 | 			},
601 | 			ExpectError:    false,
602 | 			ExpectedResult: ordersResp,
603 | 		},
604 | 		{
605 | 			Name: "successful fetch all orders with pagination",
606 | 			Request: map[string]interface{}{
607 | 				"count": 2,
608 | 				"skip":  1,
609 | 			},
610 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
611 | 				return mock.NewHTTPClient(
612 | 					mock.Endpoint{
613 | 						Path:     fetchAllOrdersPath,
614 | 						Method:   "GET",
615 | 						Response: ordersResp,
616 | 					},
617 | 				)
618 | 			},
619 | 			ExpectError:    false,
620 | 			ExpectedResult: ordersResp,
621 | 		},
622 | 		{
623 | 			Name: "successful fetch all orders with time range",
624 | 			Request: map[string]interface{}{
625 | 				"from": 1580000000,
626 | 				"to":   1590000000,
627 | 			},
628 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
629 | 				return mock.NewHTTPClient(
630 | 					mock.Endpoint{
631 | 						Path:     fetchAllOrdersPath,
632 | 						Method:   "GET",
633 | 						Response: ordersResp,
634 | 					},
635 | 				)
636 | 			},
637 | 			ExpectError:    false,
638 | 			ExpectedResult: ordersResp,
639 | 		},
640 | 		{
641 | 			Name: "successful fetch all orders with filtering",
642 | 			Request: map[string]interface{}{
643 | 				"authorized": 1,
644 | 				"receipt":    "Receipt No. 1",
645 | 			},
646 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
647 | 				return mock.NewHTTPClient(
648 | 					mock.Endpoint{
649 | 						Path:     fetchAllOrdersPath,
650 | 						Method:   "GET",
651 | 						Response: ordersResp,
652 | 					},
653 | 				)
654 | 			},
655 | 			ExpectError:    false,
656 | 			ExpectedResult: ordersResp,
657 | 		},
658 | 		{
659 | 			Name: "successful fetch all orders with expand",
660 | 			Request: map[string]interface{}{
661 | 				"expand": []interface{}{"payments"},
662 | 			},
663 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
664 | 				return mock.NewHTTPClient(
665 | 					mock.Endpoint{
666 | 						Path:     fetchAllOrdersPath,
667 | 						Method:   "GET",
668 | 						Response: ordersResp,
669 | 					},
670 | 				)
671 | 			},
672 | 			ExpectError:    false,
673 | 			ExpectedResult: ordersResp,
674 | 		},
675 | 		{
676 | 			Name: "multiple validation errors",
677 | 			Request: map[string]interface{}{
678 | 				"count":  "not-a-number",
679 | 				"skip":   "not-a-number",
680 | 				"from":   "not-a-number",
681 | 				"to":     "not-a-number",
682 | 				"expand": "not-an-array",
683 | 			},
684 | 			MockHttpClient: nil, // No HTTP client needed for validation error
685 | 			ExpectError:    true,
686 | 			ExpectedErrMsg: "Validation errors:\n- " +
687 | 				"invalid parameter type: count\n- " +
688 | 				"invalid parameter type: skip\n- " +
689 | 				"invalid parameter type: from\n- " +
690 | 				"invalid parameter type: to\n- " +
691 | 				"invalid parameter type: expand",
692 | 		},
693 | 		{
694 | 			Name: "fetch all orders fails",
695 | 			Request: map[string]interface{}{
696 | 				"count": 100,
697 | 			},
698 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
699 | 				return mock.NewHTTPClient(
700 | 					mock.Endpoint{
701 | 						Path:     fetchAllOrdersPath,
702 | 						Method:   "GET",
703 | 						Response: errorResp,
704 | 					},
705 | 				)
706 | 			},
707 | 			ExpectError:    true,
708 | 			ExpectedErrMsg: "fetching orders failed: Razorpay API error: Bad request",
709 | 		},
710 | 	}
711 | 
712 | 	for _, tc := range tests {
713 | 		t.Run(tc.Name, func(t *testing.T) {
714 | 			runToolTest(t, tc, FetchAllOrders, "Order")
715 | 		})
716 | 	}
717 | }
718 | 
719 | func Test_FetchOrderPayments(t *testing.T) {
720 | 	fetchOrderPaymentsPathFmt := fmt.Sprintf(
721 | 		"/%s%s/%%s/payments",
722 | 		constants.VERSION_V1,
723 | 		constants.ORDER_URL,
724 | 	)
725 | 
726 | 	// Define the sample response for order payments
727 | 	paymentsResp := map[string]interface{}{
728 | 		"entity": "collection",
729 | 		"count":  float64(2),
730 | 		"items": []interface{}{
731 | 			map[string]interface{}{
732 | 				"id":              "pay_N8FUmetkCE2hZP",
733 | 				"entity":          "payment",
734 | 				"amount":          float64(100),
735 | 				"currency":        "INR",
736 | 				"status":          "failed",
737 | 				"order_id":        "order_N8FRN5zTm5S3wx",
738 | 				"invoice_id":      nil,
739 | 				"international":   false,
740 | 				"method":          "upi",
741 | 				"amount_refunded": float64(0),
742 | 				"refund_status":   nil,
743 | 				"captured":        false,
744 | 				"description":     nil,
745 | 				"card_id":         nil,
746 | 				"bank":            nil,
747 | 				"wallet":          nil,
748 | 				"vpa":             "failure@razorpay",
749 | 				"email":           "[email protected]",
750 | 				"contact":         "+919999999999",
751 | 				"notes": map[string]interface{}{
752 | 					"notes_key_1": "Tea, Earl Grey, Hot",
753 | 					"notes_key_2": "Tea, Earl Grey… decaf.",
754 | 				},
755 | 				"fee":               nil,
756 | 				"tax":               nil,
757 | 				"error_code":        "BAD_REQUEST_ERROR",
758 | 				"error_description": "Payment was unsuccessful due to a temporary issue.",
759 | 				"error_source":      "gateway",
760 | 				"error_step":        "payment_response",
761 | 				"error_reason":      "payment_failed",
762 | 				"acquirer_data": map[string]interface{}{
763 | 					"rrn": nil,
764 | 				},
765 | 				"created_at": float64(1701688684),
766 | 				"upi": map[string]interface{}{
767 | 					"vpa": "failure@razorpay",
768 | 				},
769 | 			},
770 | 			map[string]interface{}{
771 | 				"id":              "pay_N8FVRD1DzYzBh1",
772 | 				"entity":          "payment",
773 | 				"amount":          float64(100),
774 | 				"currency":        "INR",
775 | 				"status":          "captured",
776 | 				"order_id":        "order_N8FRN5zTm5S3wx",
777 | 				"invoice_id":      nil,
778 | 				"international":   false,
779 | 				"method":          "upi",
780 | 				"amount_refunded": float64(0),
781 | 				"refund_status":   nil,
782 | 				"captured":        true,
783 | 				"description":     nil,
784 | 				"card_id":         nil,
785 | 				"bank":            nil,
786 | 				"wallet":          nil,
787 | 				"vpa":             "success@razorpay",
788 | 				"email":           "[email protected]",
789 | 				"contact":         "+919999999999",
790 | 				"notes": map[string]interface{}{
791 | 					"notes_key_1": "Tea, Earl Grey, Hot",
792 | 					"notes_key_2": "Tea, Earl Grey… decaf.",
793 | 				},
794 | 				"fee":               float64(2),
795 | 				"tax":               float64(0),
796 | 				"error_code":        nil,
797 | 				"error_description": nil,
798 | 				"error_source":      nil,
799 | 				"error_step":        nil,
800 | 				"error_reason":      nil,
801 | 				"acquirer_data": map[string]interface{}{
802 | 					"rrn":                "267567962619",
803 | 					"upi_transaction_id": "F5B66C7C07CA6FEAD77E956DC2FC7ABE",
804 | 				},
805 | 				"created_at": float64(1701688721),
806 | 				"upi": map[string]interface{}{
807 | 					"vpa": "success@razorpay",
808 | 				},
809 | 			},
810 | 		},
811 | 	}
812 | 
813 | 	orderNotFoundResp := map[string]interface{}{
814 | 		"error": map[string]interface{}{
815 | 			"code":        "BAD_REQUEST_ERROR",
816 | 			"description": "order not found",
817 | 		},
818 | 	}
819 | 
820 | 	tests := []RazorpayToolTestCase{
821 | 		{
822 | 			Name: "successful fetch of order payments",
823 | 			Request: map[string]interface{}{
824 | 				"order_id": "order_N8FRN5zTm5S3wx",
825 | 			},
826 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
827 | 				return mock.NewHTTPClient(
828 | 					mock.Endpoint{
829 | 						Path: fmt.Sprintf(
830 | 							fetchOrderPaymentsPathFmt,
831 | 							"order_N8FRN5zTm5S3wx",
832 | 						),
833 | 						Method:   "GET",
834 | 						Response: paymentsResp,
835 | 					},
836 | 				)
837 | 			},
838 | 			ExpectError:    false,
839 | 			ExpectedResult: paymentsResp,
840 | 		},
841 | 		{
842 | 			Name: "order not found",
843 | 			Request: map[string]interface{}{
844 | 				"order_id": "order_invalid",
845 | 			},
846 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
847 | 				return mock.NewHTTPClient(
848 | 					mock.Endpoint{
849 | 						Path: fmt.Sprintf(
850 | 							fetchOrderPaymentsPathFmt,
851 | 							"order_invalid",
852 | 						),
853 | 						Method:   "GET",
854 | 						Response: orderNotFoundResp,
855 | 					},
856 | 				)
857 | 			},
858 | 			ExpectError:    true,
859 | 			ExpectedErrMsg: "fetching payments for order failed: order not found",
860 | 		},
861 | 		{
862 | 			Name:           "missing order_id parameter",
863 | 			Request:        map[string]interface{}{},
864 | 			MockHttpClient: nil, // No HTTP client needed for validation error
865 | 			ExpectError:    true,
866 | 			ExpectedErrMsg: "missing required parameter: order_id",
867 | 		},
868 | 	}
869 | 
870 | 	for _, tc := range tests {
871 | 		t.Run(tc.Name, func(t *testing.T) {
872 | 			runToolTest(t, tc, FetchOrderPayments, "Order")
873 | 		})
874 | 	}
875 | }
876 | 
877 | func Test_UpdateOrder(t *testing.T) {
878 | 	updateOrderPathFmt := fmt.Sprintf(
879 | 		"/%s%s/%%s",
880 | 		constants.VERSION_V1,
881 | 		constants.ORDER_URL,
882 | 	)
883 | 
884 | 	updatedOrderResp := map[string]interface{}{
885 | 		"id":         "order_EKwxwAgItmmXdp",
886 | 		"entity":     "order",
887 | 		"amount":     float64(10000),
888 | 		"currency":   "INR",
889 | 		"receipt":    "receipt-123",
890 | 		"status":     "created",
891 | 		"attempts":   float64(0),
892 | 		"created_at": float64(1572505143),
893 | 		"notes": map[string]interface{}{
894 | 			"customer_name": "updated-customer",
895 | 			"product_name":  "updated-product",
896 | 		},
897 | 	}
898 | 
899 | 	orderNotFoundResp := map[string]interface{}{
900 | 		"error": map[string]interface{}{
901 | 			"code":        "BAD_REQUEST_ERROR",
902 | 			"description": "order not found",
903 | 		},
904 | 	}
905 | 
906 | 	tests := []RazorpayToolTestCase{
907 | 		{
908 | 			Name: "successful order update",
909 | 			Request: map[string]interface{}{
910 | 				"order_id": "order_EKwxwAgItmmXdp",
911 | 				"notes": map[string]interface{}{
912 | 					"customer_name": "updated-customer",
913 | 					"product_name":  "updated-product",
914 | 				},
915 | 			},
916 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
917 | 				return mock.NewHTTPClient(
918 | 					mock.Endpoint{
919 | 						Path: fmt.Sprintf(
920 | 							updateOrderPathFmt, "order_EKwxwAgItmmXdp"),
921 | 						Method:   "PATCH",
922 | 						Response: updatedOrderResp,
923 | 					},
924 | 				)
925 | 			},
926 | 			ExpectError:    false,
927 | 			ExpectedResult: updatedOrderResp,
928 | 		},
929 | 		{
930 | 			Name: "missing required parameters - order_id",
931 | 			Request: map[string]interface{}{
932 | 				// Missing order_id
933 | 				"notes": map[string]interface{}{
934 | 					"customer_name": "updated-customer",
935 | 					"product_name":  "updated-product",
936 | 				},
937 | 			},
938 | 			MockHttpClient: nil, // No HTTP client needed for validation error
939 | 			ExpectError:    true,
940 | 			ExpectedErrMsg: "missing required parameter: order_id",
941 | 		},
942 | 		{
943 | 			Name: "missing required parameters - notes",
944 | 			Request: map[string]interface{}{
945 | 				"order_id": "order_EKwxwAgItmmXdp",
946 | 				// Missing notes
947 | 			},
948 | 			MockHttpClient: nil, // No HTTP client needed for validation error
949 | 			ExpectError:    true,
950 | 			ExpectedErrMsg: "missing required parameter: notes",
951 | 		},
952 | 		{
953 | 			Name: "order not found",
954 | 			Request: map[string]interface{}{
955 | 				"order_id": "order_invalid_id",
956 | 				"notes": map[string]interface{}{
957 | 					"customer_name": "updated-customer",
958 | 					"product_name":  "updated-product",
959 | 				},
960 | 			},
961 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
962 | 				return mock.NewHTTPClient(
963 | 					mock.Endpoint{
964 | 						Path:     fmt.Sprintf(updateOrderPathFmt, "order_invalid_id"),
965 | 						Method:   "PATCH",
966 | 						Response: orderNotFoundResp,
967 | 					},
968 | 				)
969 | 			},
970 | 			ExpectError:    true,
971 | 			ExpectedErrMsg: "updating order failed: order not found",
972 | 		},
973 | 	}
974 | 
975 | 	for _, tc := range tests {
976 | 		t.Run(tc.Name, func(t *testing.T) {
977 | 			runToolTest(t, tc, UpdateOrder, "Order")
978 | 		})
979 | 	}
980 | }
981 | 
```

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

```go
   1 | package razorpay
   2 | 
   3 | import (
   4 | 	"context"
   5 | 	"fmt"
   6 | 	"net/http"
   7 | 	"net/url"
   8 | 	"strings"
   9 | 	"time"
  10 | 
  11 | 	rzpsdk "github.com/razorpay/razorpay-go"
  12 | 
  13 | 	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
  14 | 	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
  15 | )
  16 | 
  17 | // FetchPayment returns a tool that fetches payment details using payment_id
  18 | func FetchPayment(
  19 | 	obs *observability.Observability,
  20 | 	client *rzpsdk.Client,
  21 | ) mcpgo.Tool {
  22 | 	parameters := []mcpgo.ToolParameter{
  23 | 		mcpgo.WithString(
  24 | 			"payment_id",
  25 | 			mcpgo.Description("payment_id is unique identifier "+
  26 | 				"of the payment to be retrieved."),
  27 | 			mcpgo.Required(),
  28 | 		),
  29 | 	}
  30 | 
  31 | 	handler := func(
  32 | 		ctx context.Context,
  33 | 		r mcpgo.CallToolRequest,
  34 | 	) (*mcpgo.ToolResult, error) {
  35 | 		// Get client from context or use default
  36 | 		client, err := getClientFromContextOrDefault(ctx, client)
  37 | 		if err != nil {
  38 | 			return mcpgo.NewToolResultError(err.Error()), nil
  39 | 		}
  40 | 
  41 | 		params := make(map[string]interface{})
  42 | 
  43 | 		validator := NewValidator(&r).
  44 | 			ValidateAndAddRequiredString(params, "payment_id")
  45 | 
  46 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
  47 | 			return result, err
  48 | 		}
  49 | 
  50 | 		paymentId := params["payment_id"].(string)
  51 | 
  52 | 		payment, err := client.Payment.Fetch(paymentId, nil, nil)
  53 | 		if err != nil {
  54 | 			return mcpgo.NewToolResultError(
  55 | 				fmt.Sprintf("fetching payment failed: %s", err.Error())), nil
  56 | 		}
  57 | 
  58 | 		return mcpgo.NewToolResultJSON(payment)
  59 | 	}
  60 | 
  61 | 	return mcpgo.NewTool(
  62 | 		"fetch_payment",
  63 | 		"Use this tool to retrieve the details of a specific payment "+
  64 | 			"using its id. Amount returned is in paisa",
  65 | 		parameters,
  66 | 		handler,
  67 | 	)
  68 | }
  69 | 
  70 | // FetchPaymentCardDetails returns a tool that fetches card details
  71 | // for a payment
  72 | func FetchPaymentCardDetails(
  73 | 	obs *observability.Observability,
  74 | 	client *rzpsdk.Client,
  75 | ) mcpgo.Tool {
  76 | 	parameters := []mcpgo.ToolParameter{
  77 | 		mcpgo.WithString(
  78 | 			"payment_id",
  79 | 			mcpgo.Description("Unique identifier of the payment for which "+
  80 | 				"you want to retrieve card details. Must start with 'pay_'"),
  81 | 			mcpgo.Required(),
  82 | 		),
  83 | 	}
  84 | 
  85 | 	handler := func(
  86 | 		ctx context.Context,
  87 | 		r mcpgo.CallToolRequest,
  88 | 	) (*mcpgo.ToolResult, error) {
  89 | 		// Get client from context or use default
  90 | 		client, err := getClientFromContextOrDefault(ctx, client)
  91 | 		if err != nil {
  92 | 			return mcpgo.NewToolResultError(err.Error()), nil
  93 | 		}
  94 | 
  95 | 		params := make(map[string]interface{})
  96 | 
  97 | 		validator := NewValidator(&r).
  98 | 			ValidateAndAddRequiredString(params, "payment_id")
  99 | 
 100 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
 101 | 			return result, err
 102 | 		}
 103 | 
 104 | 		paymentId := params["payment_id"].(string)
 105 | 
 106 | 		cardDetails, err := client.Payment.FetchCardDetails(
 107 | 			paymentId, nil, nil)
 108 | 
 109 | 		if err != nil {
 110 | 			return mcpgo.NewToolResultError(
 111 | 				fmt.Sprintf("fetching card details failed: %s", err.Error())), nil
 112 | 		}
 113 | 
 114 | 		return mcpgo.NewToolResultJSON(cardDetails)
 115 | 	}
 116 | 
 117 | 	return mcpgo.NewTool(
 118 | 		"fetch_payment_card_details",
 119 | 		"Use this tool to retrieve the details of the card used to make a payment. "+
 120 | 			"Only works for payments made using a card.",
 121 | 		parameters,
 122 | 		handler,
 123 | 	)
 124 | }
 125 | 
 126 | // UpdatePayment returns a tool that updates the notes for a payment
 127 | func UpdatePayment(
 128 | 	obs *observability.Observability,
 129 | 	client *rzpsdk.Client,
 130 | ) mcpgo.Tool {
 131 | 	parameters := []mcpgo.ToolParameter{
 132 | 		mcpgo.WithString(
 133 | 			"payment_id",
 134 | 			mcpgo.Description("Unique identifier of the payment to be updated. "+
 135 | 				"Must start with 'pay_'"),
 136 | 			mcpgo.Required(),
 137 | 		),
 138 | 		mcpgo.WithObject(
 139 | 			"notes",
 140 | 			mcpgo.Description("Key-value pairs that can be used to store additional "+
 141 | 				"information about the payment. Values must be strings or integers."),
 142 | 			mcpgo.Required(),
 143 | 		),
 144 | 	}
 145 | 
 146 | 	handler := func(
 147 | 		ctx context.Context,
 148 | 		r mcpgo.CallToolRequest,
 149 | 	) (*mcpgo.ToolResult, error) {
 150 | 		// Get client from context or use default
 151 | 		client, err := getClientFromContextOrDefault(ctx, client)
 152 | 		if err != nil {
 153 | 			return mcpgo.NewToolResultError(err.Error()), nil
 154 | 		}
 155 | 
 156 | 		params := make(map[string]interface{})
 157 | 		paymentUpdateReq := make(map[string]interface{})
 158 | 
 159 | 		validator := NewValidator(&r).
 160 | 			ValidateAndAddRequiredString(params, "payment_id").
 161 | 			ValidateAndAddRequiredMap(paymentUpdateReq, "notes")
 162 | 
 163 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
 164 | 			return result, err
 165 | 		}
 166 | 
 167 | 		paymentId := params["payment_id"].(string)
 168 | 
 169 | 		// Update the payment
 170 | 		updatedPayment, err := client.Payment.Edit(paymentId, paymentUpdateReq, nil)
 171 | 		if err != nil {
 172 | 			return mcpgo.NewToolResultError(
 173 | 				fmt.Sprintf("updating payment failed: %s", err.Error())), nil
 174 | 		}
 175 | 
 176 | 		return mcpgo.NewToolResultJSON(updatedPayment)
 177 | 	}
 178 | 
 179 | 	return mcpgo.NewTool(
 180 | 		"update_payment",
 181 | 		"Use this tool to update the notes field of a payment. Notes are "+
 182 | 			"key-value pairs that can be used to store additional information.", //nolint:lll
 183 | 		parameters,
 184 | 		handler,
 185 | 	)
 186 | }
 187 | 
 188 | // CapturePayment returns a tool that captures an authorized payment
 189 | func CapturePayment(
 190 | 	obs *observability.Observability,
 191 | 	client *rzpsdk.Client,
 192 | ) mcpgo.Tool {
 193 | 	parameters := []mcpgo.ToolParameter{
 194 | 		mcpgo.WithString(
 195 | 			"payment_id",
 196 | 			mcpgo.Description("Unique identifier of the payment to be captured. Should start with 'pay_'"), //nolint:lll
 197 | 			mcpgo.Required(),
 198 | 		),
 199 | 		mcpgo.WithNumber(
 200 | 			"amount",
 201 | 			mcpgo.Description("The amount to be captured (in paisa). "+
 202 | 				"Should be equal to the authorized amount"),
 203 | 			mcpgo.Required(),
 204 | 		),
 205 | 		mcpgo.WithString(
 206 | 			"currency",
 207 | 			mcpgo.Description("ISO code of the currency in which the payment "+
 208 | 				"was made (e.g., INR)"),
 209 | 			mcpgo.Required(),
 210 | 		),
 211 | 	}
 212 | 
 213 | 	handler := func(
 214 | 		ctx context.Context,
 215 | 		r mcpgo.CallToolRequest,
 216 | 	) (*mcpgo.ToolResult, error) {
 217 | 		// Get client from context or use default
 218 | 		client, err := getClientFromContextOrDefault(ctx, client)
 219 | 		if err != nil {
 220 | 			return mcpgo.NewToolResultError(err.Error()), nil
 221 | 		}
 222 | 
 223 | 		params := make(map[string]interface{})
 224 | 		paymentCaptureReq := make(map[string]interface{})
 225 | 
 226 | 		validator := NewValidator(&r).
 227 | 			ValidateAndAddRequiredString(params, "payment_id").
 228 | 			ValidateAndAddRequiredInt(params, "amount").
 229 | 			ValidateAndAddRequiredString(paymentCaptureReq, "currency")
 230 | 
 231 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
 232 | 			return result, err
 233 | 		}
 234 | 
 235 | 		paymentId := params["payment_id"].(string)
 236 | 		amount := int(params["amount"].(int64))
 237 | 
 238 | 		// Capture the payment
 239 | 		payment, err := client.Payment.Capture(
 240 | 			paymentId,
 241 | 			amount,
 242 | 			paymentCaptureReq,
 243 | 			nil,
 244 | 		)
 245 | 		if err != nil {
 246 | 			return mcpgo.NewToolResultError(
 247 | 				fmt.Sprintf("capturing payment failed: %s", err.Error())), nil
 248 | 		}
 249 | 
 250 | 		return mcpgo.NewToolResultJSON(payment)
 251 | 	}
 252 | 
 253 | 	return mcpgo.NewTool(
 254 | 		"capture_payment",
 255 | 		"Use this tool to capture a previously authorized payment. Only payments with 'authorized' status can be captured", //nolint:lll
 256 | 		parameters,
 257 | 		handler,
 258 | 	)
 259 | }
 260 | 
 261 | // FetchAllPayments returns a tool to fetch multiple payments with filtering and pagination
 262 | //
 263 | //nolint:lll
 264 | func FetchAllPayments(
 265 | 	obs *observability.Observability,
 266 | 	client *rzpsdk.Client,
 267 | ) mcpgo.Tool {
 268 | 	parameters := []mcpgo.ToolParameter{
 269 | 		// Pagination parameters
 270 | 		mcpgo.WithNumber(
 271 | 			"count",
 272 | 			mcpgo.Description("Number of payments to fetch "+
 273 | 				"(default: 10, max: 100)"),
 274 | 			mcpgo.Min(1),
 275 | 			mcpgo.Max(100),
 276 | 		),
 277 | 		mcpgo.WithNumber(
 278 | 			"skip",
 279 | 			mcpgo.Description("Number of payments to skip (default: 0)"),
 280 | 			mcpgo.Min(0),
 281 | 		),
 282 | 		// Time range filters
 283 | 		mcpgo.WithNumber(
 284 | 			"from",
 285 | 			mcpgo.Description("Unix timestamp (in seconds) from when "+
 286 | 				"payments are to be fetched"),
 287 | 			mcpgo.Min(0),
 288 | 		),
 289 | 		mcpgo.WithNumber(
 290 | 			"to",
 291 | 			mcpgo.Description("Unix timestamp (in seconds) up till when "+
 292 | 				"payments are to be fetched"),
 293 | 			mcpgo.Min(0),
 294 | 		),
 295 | 	}
 296 | 
 297 | 	handler := func(
 298 | 		ctx context.Context,
 299 | 		r mcpgo.CallToolRequest,
 300 | 	) (*mcpgo.ToolResult, error) {
 301 | 		// Get client from context or use default
 302 | 		client, err := getClientFromContextOrDefault(ctx, client)
 303 | 		if err != nil {
 304 | 			return mcpgo.NewToolResultError(err.Error()), nil
 305 | 		}
 306 | 
 307 | 		// Create query parameters map
 308 | 		paymentListOptions := make(map[string]interface{})
 309 | 
 310 | 		validator := NewValidator(&r).
 311 | 			ValidateAndAddPagination(paymentListOptions).
 312 | 			ValidateAndAddOptionalInt(paymentListOptions, "from").
 313 | 			ValidateAndAddOptionalInt(paymentListOptions, "to")
 314 | 
 315 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
 316 | 			return result, err
 317 | 		}
 318 | 
 319 | 		// Fetch all payments using Razorpay SDK
 320 | 		payments, err := client.Payment.All(paymentListOptions, nil)
 321 | 		if err != nil {
 322 | 			return mcpgo.NewToolResultError(
 323 | 				fmt.Sprintf("fetching payments failed: %s", err.Error())), nil
 324 | 		}
 325 | 
 326 | 		return mcpgo.NewToolResultJSON(payments)
 327 | 	}
 328 | 
 329 | 	return mcpgo.NewTool(
 330 | 		"fetch_all_payments",
 331 | 		"Fetch all payments with optional filtering and pagination",
 332 | 		parameters,
 333 | 		handler,
 334 | 	)
 335 | }
 336 | 
 337 | // extractPaymentID extracts the payment ID from the payment response
 338 | func extractPaymentID(payment map[string]interface{}) string {
 339 | 	if id, exists := payment["razorpay_payment_id"]; exists && id != nil {
 340 | 		return id.(string)
 341 | 	}
 342 | 	return ""
 343 | }
 344 | 
 345 | // extractNextActions extracts all available actions from the payment response
 346 | func extractNextActions(
 347 | 	payment map[string]interface{},
 348 | ) []map[string]interface{} {
 349 | 	var actions []map[string]interface{}
 350 | 	if nextArray, exists := payment["next"]; exists && nextArray != nil {
 351 | 		if nextSlice, ok := nextArray.([]interface{}); ok {
 352 | 			for _, item := range nextSlice {
 353 | 				if nextItem, ok := item.(map[string]interface{}); ok {
 354 | 					actions = append(actions, nextItem)
 355 | 				}
 356 | 			}
 357 | 		}
 358 | 	}
 359 | 	return actions
 360 | }
 361 | 
 362 | // OTPResponse represents the response from OTP generation API
 363 | 
 364 | // sendOtp sends an OTP to the customer and returns the response
 365 | func sendOtp(otpUrl string) error {
 366 | 	if otpUrl == "" {
 367 | 		return fmt.Errorf("OTP URL is empty")
 368 | 	}
 369 | 	// Validate URL is safe and from Razorpay domain for security
 370 | 	parsedURL, err := url.Parse(otpUrl)
 371 | 	if err != nil {
 372 | 		return fmt.Errorf("invalid OTP URL: %s", err.Error())
 373 | 	}
 374 | 
 375 | 	if parsedURL.Scheme != "https" {
 376 | 		return fmt.Errorf("OTP URL must use HTTPS")
 377 | 	}
 378 | 
 379 | 	if !strings.Contains(parsedURL.Host, "razorpay.com") {
 380 | 		return fmt.Errorf("OTP URL must be from Razorpay domain")
 381 | 	}
 382 | 
 383 | 	// Create a secure HTTP client with timeout
 384 | 	client := &http.Client{
 385 | 		Timeout: 10 * time.Second,
 386 | 	}
 387 | 
 388 | 	req, err := http.NewRequest("POST", otpUrl, nil)
 389 | 	if err != nil {
 390 | 		return fmt.Errorf("failed to create OTP request: %s", err.Error())
 391 | 	}
 392 | 	req.Header.Set("Content-Type", "application/json")
 393 | 
 394 | 	resp, err := client.Do(req)
 395 | 	if err != nil {
 396 | 		return fmt.Errorf("OTP generation failed: %s", err.Error())
 397 | 	}
 398 | 	defer resp.Body.Close()
 399 | 
 400 | 	// Validate HTTP response status
 401 | 	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
 402 | 		return fmt.Errorf("OTP generation failed with HTTP status: %d",
 403 | 			resp.StatusCode)
 404 | 	}
 405 | 	return nil
 406 | }
 407 | 
 408 | // buildInitiatePaymentResponse constructs the response for initiate payment
 409 | func buildInitiatePaymentResponse(
 410 | 	payment map[string]interface{},
 411 | 	paymentID string,
 412 | 	actions []map[string]interface{},
 413 | ) (map[string]interface{}, string) {
 414 | 	response := map[string]interface{}{
 415 | 		"razorpay_payment_id": paymentID,
 416 | 		"payment_details":     payment,
 417 | 		"status":              "payment_initiated",
 418 | 		"message": "Payment initiated successfully using " +
 419 | 			"S2S JSON v1 flow",
 420 | 	}
 421 | 	otpUrl := ""
 422 | 
 423 | 	if len(actions) > 0 {
 424 | 		response["available_actions"] = actions
 425 | 
 426 | 		// Add guidance based on available actions
 427 | 		var actionTypes []string
 428 | 		hasOTP := false
 429 | 		hasRedirect := false
 430 | 		hasUPICollect := false
 431 | 		hasUPIIntent := false
 432 | 
 433 | 		for _, action := range actions {
 434 | 			if actionType, exists := action["action"]; exists {
 435 | 				actionStr := actionType.(string)
 436 | 				actionTypes = append(actionTypes, actionStr)
 437 | 				if actionStr == "otp_generate" {
 438 | 					hasOTP = true
 439 | 					otpUrl = action["url"].(string)
 440 | 				}
 441 | 
 442 | 				if actionStr == "redirect" {
 443 | 					hasRedirect = true
 444 | 				}
 445 | 
 446 | 				if actionStr == "upi_collect" {
 447 | 					hasUPICollect = true
 448 | 				}
 449 | 
 450 | 				if actionStr == "upi_intent" {
 451 | 					hasUPIIntent = true
 452 | 				}
 453 | 			}
 454 | 		}
 455 | 
 456 | 		switch {
 457 | 		case hasOTP:
 458 | 			response["message"] = "Payment initiated. OTP authentication is " +
 459 | 				"available. " +
 460 | 				"Use the 'submit_otp' tool to submit OTP received by the customer " +
 461 | 				"for authentication."
 462 | 			addNextStepInstructions(response, paymentID)
 463 | 		case hasRedirect:
 464 | 			response["message"] = "Payment initiated. Redirect authentication is " +
 465 | 				"available. Use the redirect URL provided in available_actions."
 466 | 		case hasUPICollect:
 467 | 			response["message"] = fmt.Sprintf(
 468 | 				"Payment initiated. Available actions: %v", actionTypes)
 469 | 		case hasUPIIntent:
 470 | 			response["message"] = fmt.Sprintf(
 471 | 				"Payment initiated. Available actions: %v", actionTypes)
 472 | 		default:
 473 | 			response["message"] = fmt.Sprintf(
 474 | 				"Payment initiated. Available actions: %v", actionTypes)
 475 | 		}
 476 | 	} else {
 477 | 		addFallbackNextStepInstructions(response, paymentID)
 478 | 	}
 479 | 
 480 | 	return response, otpUrl
 481 | }
 482 | 
 483 | // addNextStepInstructions adds next step guidance to the response
 484 | func addNextStepInstructions(
 485 | 	response map[string]interface{},
 486 | 	paymentID string,
 487 | ) {
 488 | 	if paymentID != "" {
 489 | 		response["next_step"] = "Use 'resend_otp' to regenerate OTP or " +
 490 | 			"'submit_otp' to proceed to enter OTP."
 491 | 		response["next_tool"] = "resend_otp"
 492 | 		response["next_tool_params"] = map[string]interface{}{
 493 | 			"payment_id": paymentID,
 494 | 		}
 495 | 	}
 496 | }
 497 | 
 498 | // addFallbackNextStepInstructions adds fallback next step guidance
 499 | func addFallbackNextStepInstructions(
 500 | 	response map[string]interface{},
 501 | 	paymentID string,
 502 | ) {
 503 | 	if paymentID != "" {
 504 | 		response["next_step"] = "Use 'resend_otp' to regenerate OTP or " +
 505 | 			"'submit_otp' to proceed to enter OTP if " +
 506 | 			"OTP authentication is required."
 507 | 		response["next_tool"] = "resend_otp"
 508 | 		response["next_tool_params"] = map[string]interface{}{
 509 | 			"payment_id": paymentID,
 510 | 		}
 511 | 	}
 512 | }
 513 | 
 514 | // addContactAndEmailToPaymentData adds contact and email to payment data
 515 | func addContactAndEmailToPaymentData(
 516 | 	paymentData map[string]interface{},
 517 | 	params map[string]interface{},
 518 | ) {
 519 | 	// Add contact if provided
 520 | 	if contact, exists := params["contact"]; exists && contact != "" {
 521 | 		paymentData["contact"] = contact
 522 | 	}
 523 | 
 524 | 	// Add email if provided, otherwise generate from contact
 525 | 	if email, exists := params["email"]; exists && email != "" {
 526 | 		paymentData["email"] = email
 527 | 	} else if contact, exists := paymentData["contact"]; exists && contact != "" {
 528 | 		paymentData["email"] = contact.(string) + "@mcp.razorpay.com"
 529 | 	}
 530 | }
 531 | 
 532 | // addAdditionalPaymentParameters adds additional parameters for UPI collect
 533 | // and other flows
 534 | func addAdditionalPaymentParameters(
 535 | 	paymentData map[string]interface{},
 536 | 	params map[string]interface{},
 537 | ) {
 538 | 	// Note: customer_id is now handled explicitly in buildPaymentData
 539 | 
 540 | 	// Add method if provided
 541 | 	if method, exists := params["method"]; exists && method != "" {
 542 | 		paymentData["method"] = method
 543 | 	}
 544 | 
 545 | 	// Add save if provided
 546 | 	if save, exists := params["save"]; exists {
 547 | 		paymentData["save"] = save
 548 | 	}
 549 | 
 550 | 	// Add recurring if provided
 551 | 	if recurring, exists := params["recurring"]; exists {
 552 | 		paymentData["recurring"] = recurring
 553 | 	}
 554 | 
 555 | 	// Add UPI parameters if provided
 556 | 	if upiParams, exists := params["upi"]; exists && upiParams != nil {
 557 | 		if upiMap, ok := upiParams.(map[string]interface{}); ok {
 558 | 			paymentData["upi"] = upiMap
 559 | 		}
 560 | 	}
 561 | }
 562 | 
 563 | // processUPIParameters handles VPA and UPI intent parameter processing
 564 | func processUPIParameters(params map[string]interface{}) {
 565 | 	vpa, hasVPA := params["vpa"]
 566 | 	upiIntent, hasUPIIntent := params["upi_intent"]
 567 | 
 568 | 	// Handle VPA parameter (UPI collect flow)
 569 | 	if hasVPA && vpa != "" {
 570 | 		// Set method to UPI
 571 | 		params["method"] = "upi"
 572 | 		// Set UPI parameters for collect flow
 573 | 		params["upi"] = map[string]interface{}{
 574 | 			"flow":        "collect",
 575 | 			"expiry_time": "6",
 576 | 			"vpa":         vpa,
 577 | 		}
 578 | 	}
 579 | 
 580 | 	// Handle UPI intent parameter (UPI intent flow)
 581 | 	if hasUPIIntent && upiIntent == true {
 582 | 		// Set method to UPI
 583 | 		params["method"] = "upi"
 584 | 		// Set UPI parameters for intent flow
 585 | 		params["upi"] = map[string]interface{}{
 586 | 			"flow": "intent",
 587 | 		}
 588 | 	}
 589 | }
 590 | 
 591 | // createOrGetCustomer creates or gets a customer if contact is provided
 592 | func createOrGetCustomer(
 593 | 	client *rzpsdk.Client,
 594 | 	params map[string]interface{},
 595 | ) (map[string]interface{}, error) {
 596 | 	contactValue, exists := params["contact"]
 597 | 	if !exists || contactValue == "" {
 598 | 		return nil, nil
 599 | 	}
 600 | 
 601 | 	contact := contactValue.(string)
 602 | 	customerData := map[string]interface{}{
 603 | 		"contact":       contact,
 604 | 		"fail_existing": "0", // Get existing customer if exists
 605 | 	}
 606 | 
 607 | 	// Create/get customer using Razorpay SDK
 608 | 	customer, err := client.Customer.Create(customerData, nil)
 609 | 	if err != nil {
 610 | 		return nil, fmt.Errorf(
 611 | 			"failed to create/fetch customer with contact %s: %v",
 612 | 			contact,
 613 | 			err,
 614 | 		)
 615 | 	}
 616 | 	return customer, nil
 617 | }
 618 | 
 619 | // buildPaymentData constructs the payment data for the API call
 620 | func buildPaymentData(
 621 | 	params map[string]interface{},
 622 | 	currency string,
 623 | 	customerId string,
 624 | ) *map[string]interface{} {
 625 | 	paymentData := map[string]interface{}{
 626 | 		"amount":   params["amount"],
 627 | 		"currency": currency,
 628 | 		"order_id": params["order_id"],
 629 | 	}
 630 | 	if customerId != "" {
 631 | 		paymentData["customer_id"] = customerId
 632 | 	}
 633 | 
 634 | 	// Add token if provided (required for saved payment methods,
 635 | 	// optional for UPI collect)
 636 | 	if token, exists := params["token"]; exists && token != "" {
 637 | 		paymentData["token"] = token
 638 | 	}
 639 | 
 640 | 	// Add contact and email parameters
 641 | 	addContactAndEmailToPaymentData(paymentData, params)
 642 | 
 643 | 	// Add additional parameters for UPI collect and other flows
 644 | 	addAdditionalPaymentParameters(paymentData, params)
 645 | 
 646 | 	// Add force_terminal_id if provided (for single block multiple debit orders)
 647 | 	if terminalID, exists := params["force_terminal_id"]; exists &&
 648 | 		terminalID != "" {
 649 | 		paymentData["force_terminal_id"] = terminalID
 650 | 	}
 651 | 
 652 | 	return &paymentData
 653 | }
 654 | 
 655 | // processPaymentResult processes the payment creation result
 656 | func processPaymentResult(
 657 | 	payment map[string]interface{},
 658 | ) (map[string]interface{}, error) {
 659 | 	// Extract payment ID and next actions from the response
 660 | 	paymentID := extractPaymentID(payment)
 661 | 	actions := extractNextActions(payment)
 662 | 
 663 | 	// Build structured response using the helper function
 664 | 	response, otpUrl := buildInitiatePaymentResponse(payment, paymentID, actions)
 665 | 
 666 | 	// Only send OTP if there's an OTP URL
 667 | 	if otpUrl != "" {
 668 | 		err := sendOtp(otpUrl)
 669 | 		if err != nil {
 670 | 			return nil, fmt.Errorf("OTP generation failed: %s", err.Error())
 671 | 		}
 672 | 	}
 673 | 
 674 | 	return response, nil
 675 | }
 676 | 
 677 | // InitiatePayment returns a tool that initiates a payment using order_id
 678 | // and token
 679 | // This implements the S2S JSON v1 flow for creating payments
 680 | func InitiatePayment(
 681 | 	obs *observability.Observability,
 682 | 	client *rzpsdk.Client,
 683 | ) mcpgo.Tool {
 684 | 	parameters := []mcpgo.ToolParameter{
 685 | 		mcpgo.WithNumber(
 686 | 			"amount",
 687 | 			mcpgo.Description("Payment amount in the smallest currency sub-unit "+
 688 | 				"(e.g., for ₹100, use 10000)"),
 689 | 			mcpgo.Required(),
 690 | 			mcpgo.Min(100),
 691 | 		),
 692 | 		mcpgo.WithString(
 693 | 			"currency",
 694 | 			mcpgo.Description("Currency code for the payment. Default is 'INR'"),
 695 | 		),
 696 | 		mcpgo.WithString(
 697 | 			"token",
 698 | 			mcpgo.Description("Token ID of the saved payment method. "+
 699 | 				"Must start with 'token_'"),
 700 | 		),
 701 | 		mcpgo.WithString(
 702 | 			"order_id",
 703 | 			mcpgo.Description("Order ID for which the payment is being initiated. "+
 704 | 				"Must start with 'order_'"),
 705 | 			mcpgo.Required(),
 706 | 		),
 707 | 		mcpgo.WithString(
 708 | 			"email",
 709 | 			mcpgo.Description("Customer's email address (optional)"),
 710 | 		),
 711 | 		mcpgo.WithString(
 712 | 			"contact",
 713 | 			mcpgo.Description("Customer's phone number"),
 714 | 		),
 715 | 		mcpgo.WithString(
 716 | 			"customer_id",
 717 | 			mcpgo.Description("Customer ID for the payment. "+
 718 | 				"Must start with 'cust_'"),
 719 | 		),
 720 | 		mcpgo.WithBoolean(
 721 | 			"save",
 722 | 			mcpgo.Description("Whether to save the payment method for future use"),
 723 | 		),
 724 | 		mcpgo.WithString(
 725 | 			"vpa",
 726 | 			mcpgo.Description("Virtual Payment Address (VPA) for UPI payment. "+
 727 | 				"When provided, automatically sets method='upi' and UPI parameters "+
 728 | 				"with flow='collect' and expiry_time='6' (e.g., '9876543210@ptsbi')"),
 729 | 		),
 730 | 		mcpgo.WithBoolean(
 731 | 			"upi_intent",
 732 | 			mcpgo.Description("Enable UPI intent flow. "+
 733 | 				"When set to true, automatically sets method='upi' and UPI parameters "+
 734 | 				"with flow='intent'. The API will return a UPI URL in the response."),
 735 | 		),
 736 | 		mcpgo.WithBoolean(
 737 | 			"recurring",
 738 | 			mcpgo.Description("Set this to true for recurring payments like "+
 739 | 				"single block multiple debit."),
 740 | 		),
 741 | 		mcpgo.WithString(
 742 | 			"force_terminal_id",
 743 | 			mcpgo.Description("Terminal ID to be passed in case of single block "+
 744 | 				"multiple debit order."),
 745 | 		),
 746 | 	}
 747 | 
 748 | 	handler := func(
 749 | 		ctx context.Context,
 750 | 		r mcpgo.CallToolRequest,
 751 | 	) (*mcpgo.ToolResult, error) {
 752 | 		// Get client from context or use default
 753 | 		client, err := getClientFromContextOrDefault(ctx, client)
 754 | 		if err != nil {
 755 | 			return mcpgo.NewToolResultError(err.Error()), nil
 756 | 		}
 757 | 
 758 | 		params := make(map[string]interface{})
 759 | 
 760 | 		validator := NewValidator(&r).
 761 | 			ValidateAndAddRequiredInt(params, "amount").
 762 | 			ValidateAndAddOptionalString(params, "currency").
 763 | 			ValidateAndAddOptionalString(params, "token").
 764 | 			ValidateAndAddRequiredString(params, "order_id").
 765 | 			ValidateAndAddOptionalString(params, "email").
 766 | 			ValidateAndAddOptionalString(params, "contact").
 767 | 			ValidateAndAddOptionalString(params, "customer_id").
 768 | 			ValidateAndAddOptionalBool(params, "save").
 769 | 			ValidateAndAddOptionalString(params, "vpa").
 770 | 			ValidateAndAddOptionalBool(params, "upi_intent").
 771 | 			ValidateAndAddOptionalBool(params, "recurring").
 772 | 			ValidateAndAddOptionalString(params, "force_terminal_id")
 773 | 
 774 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
 775 | 			return result, err
 776 | 		}
 777 | 
 778 | 		// Set default currency
 779 | 		currency := "INR"
 780 | 		if c, exists := params["currency"]; exists && c != "" {
 781 | 			currency = c.(string)
 782 | 		}
 783 | 
 784 | 		// Process UPI parameters (VPA for collect flow, upi_intent for intent flow)
 785 | 		processUPIParameters(params)
 786 | 
 787 | 		// Handle customer ID
 788 | 		var customerID string
 789 | 		if custID, exists := params["customer_id"]; exists && custID != "" {
 790 | 			customerID = custID.(string)
 791 | 		} else {
 792 | 			// Create or get customer if contact is provided
 793 | 			customer, err := createOrGetCustomer(client, params)
 794 | 			if err != nil {
 795 | 				return mcpgo.NewToolResultError(err.Error()), nil
 796 | 			}
 797 | 			if customer != nil {
 798 | 				if id, ok := customer["id"].(string); ok {
 799 | 					customerID = id
 800 | 				}
 801 | 			}
 802 | 		}
 803 | 
 804 | 		// Build payment data
 805 | 		paymentDataPtr := buildPaymentData(params, currency, customerID)
 806 | 		paymentData := *paymentDataPtr
 807 | 
 808 | 		// Create payment using Razorpay SDK's CreatePaymentJson method
 809 | 		// This follows the S2S JSON v1 flow:
 810 | 		// https://api.razorpay.com/v1/payments/create/json
 811 | 		payment, err := client.Payment.CreatePaymentJson(paymentData, nil)
 812 | 		if err != nil {
 813 | 			return mcpgo.NewToolResultError(
 814 | 				fmt.Sprintf("initiating payment failed: %s", err.Error())), nil
 815 | 		}
 816 | 
 817 | 		// Process payment result
 818 | 		response, err := processPaymentResult(payment)
 819 | 		if err != nil {
 820 | 			return mcpgo.NewToolResultError(err.Error()), nil
 821 | 		}
 822 | 
 823 | 		return mcpgo.NewToolResultJSON(response)
 824 | 	}
 825 | 
 826 | 	return mcpgo.NewTool(
 827 | 		"initiate_payment",
 828 | 		"Initiate a payment using the S2S JSON v1 flow. "+
 829 | 			"Required parameters: amount and order_id. "+
 830 | 			"For saved payment methods, provide token. "+
 831 | 			"For UPI collect flow, provide 'vpa' parameter "+
 832 | 			"which automatically sets UPI with flow='collect' and expiry_time='6'. "+
 833 | 			"For UPI intent flow, set 'upi_intent=true' parameter "+
 834 | 			"which automatically sets UPI with flow='intent' and API returns UPI URL. "+
 835 | 			"Supports additional parameters like customer_id, email, "+
 836 | 			"contact, save, and recurring. "+
 837 | 			"Returns payment details including next action steps if required.",
 838 | 		parameters,
 839 | 		handler,
 840 | 	)
 841 | }
 842 | 
 843 | // ResendOtp returns a tool that sends OTP for payment authentication
 844 | func ResendOtp(
 845 | 	obs *observability.Observability,
 846 | 	client *rzpsdk.Client,
 847 | ) mcpgo.Tool {
 848 | 	parameters := []mcpgo.ToolParameter{
 849 | 		mcpgo.WithString(
 850 | 			"payment_id",
 851 | 			mcpgo.Description("Unique identifier of the payment for which "+
 852 | 				"OTP needs to be generated. Must start with 'pay_'"),
 853 | 			mcpgo.Required(),
 854 | 		),
 855 | 	}
 856 | 
 857 | 	handler := func(
 858 | 		ctx context.Context,
 859 | 		r mcpgo.CallToolRequest,
 860 | 	) (*mcpgo.ToolResult, error) {
 861 | 
 862 | 		// Get client from context or use default
 863 | 		client, err := getClientFromContextOrDefault(ctx, client)
 864 | 		if err != nil {
 865 | 			return mcpgo.NewToolResultError(err.Error()), nil
 866 | 		}
 867 | 
 868 | 		params := make(map[string]interface{})
 869 | 
 870 | 		validator := NewValidator(&r).
 871 | 			ValidateAndAddRequiredString(params, "payment_id")
 872 | 
 873 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
 874 | 			return result, err
 875 | 		}
 876 | 
 877 | 		paymentID := params["payment_id"].(string)
 878 | 
 879 | 		// Resend OTP using Razorpay SDK
 880 | 		otpResponse, err := client.Payment.OtpResend(paymentID, nil, nil)
 881 | 		if err != nil {
 882 | 			return mcpgo.NewToolResultError(
 883 | 				fmt.Sprintf("OTP resend failed: %s", err.Error())), nil
 884 | 		}
 885 | 
 886 | 		// Extract OTP submit URL from response
 887 | 		otpSubmitURL := extractOtpSubmitURL(otpResponse)
 888 | 
 889 | 		// Prepare response
 890 | 		response := map[string]interface{}{
 891 | 			"payment_id": paymentID,
 892 | 			"status":     "success",
 893 | 			"message": "OTP sent successfully. Please enter the OTP received on your " +
 894 | 				"mobile number to complete the payment.",
 895 | 			"response_data": otpResponse,
 896 | 		}
 897 | 
 898 | 		// Add next step instructions if OTP submit URL is available
 899 | 		if otpSubmitURL != "" {
 900 | 			response["otp_submit_url"] = otpSubmitURL
 901 | 			response["next_step"] = "Use 'submit_otp' tool with the OTP code received " +
 902 | 				"from user to complete payment authentication."
 903 | 			response["next_tool"] = "submit_otp"
 904 | 			response["next_tool_params"] = map[string]interface{}{
 905 | 				"payment_id": paymentID,
 906 | 				"otp_string": "{OTP_CODE_FROM_USER}",
 907 | 			}
 908 | 		} else {
 909 | 			response["next_step"] = "Use 'submit_otp' tool with the OTP code received " +
 910 | 				"from user to complete payment authentication."
 911 | 			response["next_tool"] = "submit_otp"
 912 | 			response["next_tool_params"] = map[string]interface{}{
 913 | 				"payment_id": paymentID,
 914 | 				"otp_string": "{OTP_CODE_FROM_USER}",
 915 | 			}
 916 | 		}
 917 | 
 918 | 		result, err := mcpgo.NewToolResultJSON(response)
 919 | 		if err != nil {
 920 | 			return mcpgo.NewToolResultError(
 921 | 				fmt.Sprintf("JSON marshal error: %v", err)), nil
 922 | 		}
 923 | 		return result, nil
 924 | 	}
 925 | 
 926 | 	return mcpgo.NewTool(
 927 | 		"resend_otp",
 928 | 		"Resend OTP to the customer's registered mobile number if the previous "+
 929 | 			"OTP was not received or has expired.",
 930 | 		parameters,
 931 | 		handler,
 932 | 	)
 933 | }
 934 | 
 935 | // SubmitOtp returns a tool that submits OTP for payment verification
 936 | func SubmitOtp(
 937 | 	obs *observability.Observability,
 938 | 	client *rzpsdk.Client,
 939 | ) mcpgo.Tool {
 940 | 	parameters := []mcpgo.ToolParameter{
 941 | 		mcpgo.WithString(
 942 | 			"otp_string",
 943 | 			mcpgo.Description("OTP string received from the user"),
 944 | 			mcpgo.Required(),
 945 | 		),
 946 | 		mcpgo.WithString(
 947 | 			"payment_id",
 948 | 			mcpgo.Description("Unique identifier of the payment for which "+
 949 | 				"OTP needs to be submitted. Must start with 'pay_'"),
 950 | 			mcpgo.Required(),
 951 | 		),
 952 | 	}
 953 | 
 954 | 	handler := func(
 955 | 		ctx context.Context,
 956 | 		r mcpgo.CallToolRequest,
 957 | 	) (*mcpgo.ToolResult, error) {
 958 | 		// Get client from context or use default
 959 | 		client, err := getClientFromContextOrDefault(ctx, client)
 960 | 		if err != nil {
 961 | 			return mcpgo.NewToolResultError(err.Error()), nil
 962 | 		}
 963 | 
 964 | 		params := make(map[string]interface{})
 965 | 
 966 | 		validator := NewValidator(&r).
 967 | 			ValidateAndAddRequiredString(params, "otp_string").
 968 | 			ValidateAndAddRequiredString(params, "payment_id")
 969 | 
 970 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
 971 | 			return result, err
 972 | 		}
 973 | 
 974 | 		paymentID := params["payment_id"].(string)
 975 | 		data := map[string]interface{}{
 976 | 			"otp": params["otp_string"].(string),
 977 | 		}
 978 | 		otpResponse, err := client.Payment.OtpSubmit(paymentID, data, nil)
 979 | 
 980 | 		if err != nil {
 981 | 			return mcpgo.NewToolResultError(
 982 | 				fmt.Sprintf("OTP verification failed: %s", err.Error())), nil
 983 | 		}
 984 | 
 985 | 		// Prepare response
 986 | 		response := map[string]interface{}{
 987 | 			"payment_id":    paymentID,
 988 | 			"status":        "success",
 989 | 			"message":       "OTP verified successfully.",
 990 | 			"response_data": otpResponse,
 991 | 		}
 992 | 		result, err := mcpgo.NewToolResultJSON(response)
 993 | 		if err != nil {
 994 | 			return mcpgo.NewToolResultError(
 995 | 				fmt.Sprintf("JSON marshal error: %v", err)), nil
 996 | 		}
 997 | 		return result, nil
 998 | 	}
 999 | 
1000 | 	return mcpgo.NewTool(
1001 | 		"submit_otp",
1002 | 		"Verify and submit the OTP received by the customer to complete "+
1003 | 			"the payment authentication process.",
1004 | 		parameters,
1005 | 		handler,
1006 | 	)
1007 | }
1008 | 
1009 | // extractOtpSubmitURL extracts the OTP submit URL from the payment response
1010 | func extractOtpSubmitURL(responseData interface{}) string {
1011 | 	jsonData, ok := responseData.(map[string]interface{})
1012 | 	if !ok {
1013 | 		return ""
1014 | 	}
1015 | 
1016 | 	nextArray, exists := jsonData["next"]
1017 | 	if !exists || nextArray == nil {
1018 | 		return ""
1019 | 	}
1020 | 
1021 | 	nextSlice, ok := nextArray.([]interface{})
1022 | 	if !ok {
1023 | 		return ""
1024 | 	}
1025 | 
1026 | 	for _, item := range nextSlice {
1027 | 		nextItem, ok := item.(map[string]interface{})
1028 | 		if !ok {
1029 | 			continue
1030 | 		}
1031 | 
1032 | 		action, exists := nextItem["action"]
1033 | 		if !exists || action != "otp_submit" {
1034 | 			continue
1035 | 		}
1036 | 
1037 | 		submitURL, exists := nextItem["url"]
1038 | 		if exists && submitURL != nil {
1039 | 			if urlStr, ok := submitURL.(string); ok {
1040 | 				return urlStr
1041 | 			}
1042 | 		}
1043 | 	}
1044 | 
1045 | 	return ""
1046 | }
1047 | 
```

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

```go
   1 | package razorpay
   2 | 
   3 | import (
   4 | 	"testing"
   5 | 
   6 | 	"github.com/stretchr/testify/assert"
   7 | 
   8 | 	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
   9 | )
  10 | 
  11 | func TestValidator(t *testing.T) {
  12 | 	tests := []struct {
  13 | 		name           string
  14 | 		args           map[string]interface{}
  15 | 		paramName      string
  16 | 		validationFunc func(*Validator, map[string]interface{}, string) *Validator
  17 | 		expectError    bool
  18 | 		expectValue    interface{}
  19 | 		expectKey      string
  20 | 	}{
  21 | 		// String tests
  22 | 		{
  23 | 			name:           "required string - valid",
  24 | 			args:           map[string]interface{}{"test_param": "test_value"},
  25 | 			paramName:      "test_param",
  26 | 			validationFunc: (*Validator).ValidateAndAddRequiredString,
  27 | 			expectError:    false,
  28 | 			expectValue:    "test_value",
  29 | 			expectKey:      "test_param",
  30 | 		},
  31 | 		{
  32 | 			name:           "required string - missing",
  33 | 			args:           map[string]interface{}{},
  34 | 			paramName:      "test_param",
  35 | 			validationFunc: (*Validator).ValidateAndAddRequiredString,
  36 | 			expectError:    true,
  37 | 			expectValue:    nil,
  38 | 			expectKey:      "test_param",
  39 | 		},
  40 | 		{
  41 | 			name:           "optional string - valid",
  42 | 			args:           map[string]interface{}{"test_param": "test_value"},
  43 | 			paramName:      "test_param",
  44 | 			validationFunc: (*Validator).ValidateAndAddOptionalString,
  45 | 			expectError:    false,
  46 | 			expectValue:    "test_value",
  47 | 			expectKey:      "test_param",
  48 | 		},
  49 | 		{
  50 | 			name:           "optional string - empty",
  51 | 			args:           map[string]interface{}{"test_param": ""},
  52 | 			paramName:      "test_param",
  53 | 			validationFunc: (*Validator).ValidateAndAddOptionalString,
  54 | 			expectError:    false,
  55 | 			expectValue:    "",
  56 | 			expectKey:      "test_param",
  57 | 		},
  58 | 
  59 | 		// Int tests
  60 | 		{
  61 | 			name:           "required int - valid",
  62 | 			args:           map[string]interface{}{"test_param": float64(123)},
  63 | 			paramName:      "test_param",
  64 | 			validationFunc: (*Validator).ValidateAndAddRequiredInt,
  65 | 			expectError:    false,
  66 | 			expectValue:    int64(123),
  67 | 			expectKey:      "test_param",
  68 | 		},
  69 | 		{
  70 | 			name:           "optional int - valid",
  71 | 			args:           map[string]interface{}{"test_param": float64(123)},
  72 | 			paramName:      "test_param",
  73 | 			validationFunc: (*Validator).ValidateAndAddOptionalInt,
  74 | 			expectError:    false,
  75 | 			expectValue:    int64(123),
  76 | 			expectKey:      "test_param",
  77 | 		},
  78 | 		{
  79 | 			name:           "optional int - zero",
  80 | 			args:           map[string]interface{}{"test_param": float64(0)},
  81 | 			paramName:      "test_param",
  82 | 			validationFunc: (*Validator).ValidateAndAddOptionalInt,
  83 | 			expectError:    false,
  84 | 			expectValue:    int64(0), // we expect the zero values as is
  85 | 			expectKey:      "test_param",
  86 | 		},
  87 | 
  88 | 		// Float tests
  89 | 		{
  90 | 			name:           "required float - valid",
  91 | 			args:           map[string]interface{}{"test_param": float64(123.45)},
  92 | 			paramName:      "test_param",
  93 | 			validationFunc: (*Validator).ValidateAndAddRequiredFloat,
  94 | 			expectError:    false,
  95 | 			expectValue:    float64(123.45),
  96 | 			expectKey:      "test_param",
  97 | 		},
  98 | 		{
  99 | 			name:           "optional float - valid",
 100 | 			args:           map[string]interface{}{"test_param": float64(123.45)},
 101 | 			paramName:      "test_param",
 102 | 			validationFunc: (*Validator).ValidateAndAddOptionalFloat,
 103 | 			expectError:    false,
 104 | 			expectValue:    float64(123.45),
 105 | 			expectKey:      "test_param",
 106 | 		},
 107 | 		{
 108 | 			name:           "optional float - zero",
 109 | 			args:           map[string]interface{}{"test_param": float64(0)},
 110 | 			paramName:      "test_param",
 111 | 			validationFunc: (*Validator).ValidateAndAddOptionalFloat,
 112 | 			expectError:    false,
 113 | 			expectValue:    float64(0),
 114 | 			expectKey:      "test_param",
 115 | 		},
 116 | 
 117 | 		// Bool tests
 118 | 		{
 119 | 			name:           "required bool - true",
 120 | 			args:           map[string]interface{}{"test_param": true},
 121 | 			paramName:      "test_param",
 122 | 			validationFunc: (*Validator).ValidateAndAddRequiredBool,
 123 | 			expectError:    false,
 124 | 			expectValue:    true,
 125 | 			expectKey:      "test_param",
 126 | 		},
 127 | 		{
 128 | 			name:           "required bool - false",
 129 | 			args:           map[string]interface{}{"test_param": false},
 130 | 			paramName:      "test_param",
 131 | 			validationFunc: (*Validator).ValidateAndAddRequiredBool,
 132 | 			expectError:    false,
 133 | 			expectValue:    false,
 134 | 			expectKey:      "test_param",
 135 | 		},
 136 | 		{
 137 | 			name:           "optional bool - true",
 138 | 			args:           map[string]interface{}{"test_param": true},
 139 | 			paramName:      "test_param",
 140 | 			validationFunc: (*Validator).ValidateAndAddOptionalBool,
 141 | 			expectError:    false,
 142 | 			expectValue:    true,
 143 | 			expectKey:      "test_param",
 144 | 		},
 145 | 		{
 146 | 			name:           "optional bool - false",
 147 | 			args:           map[string]interface{}{"test_param": false},
 148 | 			paramName:      "test_param",
 149 | 			validationFunc: (*Validator).ValidateAndAddOptionalBool,
 150 | 			expectError:    false,
 151 | 			expectValue:    false,
 152 | 			expectKey:      "test_param",
 153 | 		},
 154 | 
 155 | 		// Map tests
 156 | 		{
 157 | 			name: "required map - valid",
 158 | 			args: map[string]interface{}{
 159 | 				"test_param": map[string]interface{}{"key": "value"},
 160 | 			},
 161 | 			paramName:      "test_param",
 162 | 			validationFunc: (*Validator).ValidateAndAddRequiredMap,
 163 | 			expectError:    false,
 164 | 			expectValue:    map[string]interface{}{"key": "value"},
 165 | 			expectKey:      "test_param",
 166 | 		},
 167 | 		{
 168 | 			name: "optional map - valid",
 169 | 			args: map[string]interface{}{
 170 | 				"test_param": map[string]interface{}{"key": "value"},
 171 | 			},
 172 | 			paramName:      "test_param",
 173 | 			validationFunc: (*Validator).ValidateAndAddOptionalMap,
 174 | 			expectError:    false,
 175 | 			expectValue:    map[string]interface{}{"key": "value"},
 176 | 			expectKey:      "test_param",
 177 | 		},
 178 | 		{
 179 | 			name: "optional map - empty",
 180 | 			args: map[string]interface{}{
 181 | 				"test_param": map[string]interface{}{},
 182 | 			},
 183 | 			paramName:      "test_param",
 184 | 			validationFunc: (*Validator).ValidateAndAddOptionalMap,
 185 | 			expectError:    false,
 186 | 			expectValue:    map[string]interface{}{},
 187 | 			expectKey:      "test_param",
 188 | 		},
 189 | 
 190 | 		// Array tests
 191 | 		{
 192 | 			name: "required array - valid",
 193 | 			args: map[string]interface{}{
 194 | 				"test_param": []interface{}{"value1", "value2"},
 195 | 			},
 196 | 			paramName:      "test_param",
 197 | 			validationFunc: (*Validator).ValidateAndAddRequiredArray,
 198 | 			expectError:    false,
 199 | 			expectValue:    []interface{}{"value1", "value2"},
 200 | 			expectKey:      "test_param",
 201 | 		},
 202 | 		{
 203 | 			name: "optional array - valid",
 204 | 			args: map[string]interface{}{
 205 | 				"test_param": []interface{}{"value1", "value2"},
 206 | 			},
 207 | 			paramName:      "test_param",
 208 | 			validationFunc: (*Validator).ValidateAndAddOptionalArray,
 209 | 			expectError:    false,
 210 | 			expectValue:    []interface{}{"value1", "value2"},
 211 | 			expectKey:      "test_param",
 212 | 		},
 213 | 		{
 214 | 			name:           "optional array - empty",
 215 | 			args:           map[string]interface{}{"test_param": []interface{}{}},
 216 | 			paramName:      "test_param",
 217 | 			validationFunc: (*Validator).ValidateAndAddOptionalArray,
 218 | 			expectError:    false,
 219 | 			expectValue:    []interface{}{},
 220 | 			expectKey:      "test_param",
 221 | 		},
 222 | 
 223 | 		// Invalid type tests
 224 | 		{
 225 | 			name:           "required string - wrong type",
 226 | 			args:           map[string]interface{}{"test_param": 123},
 227 | 			paramName:      "test_param",
 228 | 			validationFunc: (*Validator).ValidateAndAddRequiredString,
 229 | 			expectError:    true,
 230 | 			expectValue:    nil,
 231 | 			expectKey:      "test_param",
 232 | 		},
 233 | 		{
 234 | 			name:           "required int - wrong type",
 235 | 			args:           map[string]interface{}{"test_param": "not a number"},
 236 | 			paramName:      "test_param",
 237 | 			validationFunc: (*Validator).ValidateAndAddRequiredInt,
 238 | 			expectError:    true,
 239 | 			expectValue:    nil,
 240 | 			expectKey:      "test_param",
 241 | 		},
 242 | 	}
 243 | 
 244 | 	for _, tt := range tests {
 245 | 		t.Run(tt.name, func(t *testing.T) {
 246 | 			result := make(map[string]interface{})
 247 | 			request := &mcpgo.CallToolRequest{
 248 | 				Arguments: tt.args,
 249 | 			}
 250 | 			validator := NewValidator(request)
 251 | 
 252 | 			tt.validationFunc(validator, result, tt.paramName)
 253 | 
 254 | 			if tt.expectError {
 255 | 				assert.True(t, validator.HasErrors(), "Expected validation error")
 256 | 			} else {
 257 | 				assert.False(t, validator.HasErrors(), "Did not expect validation error")
 258 | 				assert.Equal(t,
 259 | 					tt.expectValue,
 260 | 					result[tt.expectKey],
 261 | 					"Parameter value mismatch",
 262 | 				)
 263 | 			}
 264 | 		})
 265 | 	}
 266 | }
 267 | 
 268 | func TestValidatorPagination(t *testing.T) {
 269 | 	tests := []struct {
 270 | 		name        string
 271 | 		args        map[string]interface{}
 272 | 		expectCount interface{}
 273 | 		expectSkip  interface{}
 274 | 		expectError bool
 275 | 	}{
 276 | 		{
 277 | 			name: "valid pagination params",
 278 | 			args: map[string]interface{}{
 279 | 				"count": float64(10),
 280 | 				"skip":  float64(5),
 281 | 			},
 282 | 			expectCount: int64(10),
 283 | 			expectSkip:  int64(5),
 284 | 			expectError: false,
 285 | 		},
 286 | 		{
 287 | 			name:        "zero pagination params",
 288 | 			args:        map[string]interface{}{"count": float64(0), "skip": float64(0)},
 289 | 			expectCount: int64(0),
 290 | 			expectSkip:  int64(0),
 291 | 			expectError: false,
 292 | 		},
 293 | 		{
 294 | 			name: "invalid count type",
 295 | 			args: map[string]interface{}{
 296 | 				"count": "not a number",
 297 | 				"skip":  float64(5),
 298 | 			},
 299 | 			expectCount: nil,
 300 | 			expectSkip:  int64(5),
 301 | 			expectError: true,
 302 | 		},
 303 | 	}
 304 | 
 305 | 	for _, tt := range tests {
 306 | 		t.Run(tt.name, func(t *testing.T) {
 307 | 			result := make(map[string]interface{})
 308 | 			request := &mcpgo.CallToolRequest{
 309 | 				Arguments: tt.args,
 310 | 			}
 311 | 			validator := NewValidator(request)
 312 | 
 313 | 			validator.ValidateAndAddPagination(result)
 314 | 
 315 | 			if tt.expectError {
 316 | 				assert.True(t, validator.HasErrors(), "Expected validation error")
 317 | 			} else {
 318 | 				assert.False(t, validator.HasErrors(), "Did not expect validation error")
 319 | 			}
 320 | 
 321 | 			if tt.expectCount != nil {
 322 | 				assert.Equal(t, tt.expectCount, result["count"], "Count mismatch")
 323 | 			} else {
 324 | 				_, exists := result["count"]
 325 | 				assert.False(t, exists, "Count should not be added")
 326 | 			}
 327 | 
 328 | 			if tt.expectSkip != nil {
 329 | 				assert.Equal(t, tt.expectSkip, result["skip"], "Skip mismatch")
 330 | 			} else {
 331 | 				_, exists := result["skip"]
 332 | 				assert.False(t, exists, "Skip should not be added")
 333 | 			}
 334 | 		})
 335 | 	}
 336 | }
 337 | 
 338 | func TestValidatorExpand(t *testing.T) {
 339 | 	tests := []struct {
 340 | 		name         string
 341 | 		args         map[string]interface{}
 342 | 		expectExpand string
 343 | 		expectError  bool
 344 | 	}{
 345 | 		{
 346 | 			name:         "valid expand param",
 347 | 			args:         map[string]interface{}{"expand": []interface{}{"payments"}},
 348 | 			expectExpand: "payments",
 349 | 			expectError:  false,
 350 | 		},
 351 | 		{
 352 | 			name:         "empty expand array",
 353 | 			args:         map[string]interface{}{"expand": []interface{}{}},
 354 | 			expectExpand: "",
 355 | 			expectError:  false,
 356 | 		},
 357 | 		{
 358 | 			name:         "invalid expand type",
 359 | 			args:         map[string]interface{}{"expand": "not an array"},
 360 | 			expectExpand: "",
 361 | 			expectError:  true,
 362 | 		},
 363 | 	}
 364 | 
 365 | 	for _, tt := range tests {
 366 | 		t.Run(tt.name, func(t *testing.T) {
 367 | 			result := make(map[string]interface{})
 368 | 			request := &mcpgo.CallToolRequest{
 369 | 				Arguments: tt.args,
 370 | 			}
 371 | 			validator := NewValidator(request)
 372 | 
 373 | 			validator.ValidateAndAddExpand(result)
 374 | 
 375 | 			if tt.expectError {
 376 | 				assert.True(t, validator.HasErrors(), "Expected validation error")
 377 | 			} else {
 378 | 				assert.False(t, validator.HasErrors(), "Did not expect validation error")
 379 | 				if tt.expectExpand != "" {
 380 | 					assert.Equal(t,
 381 | 						tt.expectExpand,
 382 | 						result["expand[]"],
 383 | 						"Expand value mismatch",
 384 | 					)
 385 | 				} else {
 386 | 					_, exists := result["expand[]"]
 387 | 					assert.False(t, exists, "Expand should not be added")
 388 | 				}
 389 | 			}
 390 | 		})
 391 | 	}
 392 | }
 393 | 
 394 | // Test validator "To" functions which write to target maps
 395 | func TestValidatorToFunctions(t *testing.T) {
 396 | 	tests := []struct {
 397 | 		name      string
 398 | 		args      map[string]interface{}
 399 | 		paramName string
 400 | 		targetKey string
 401 | 		testFunc  func(
 402 | 			*Validator, map[string]interface{}, string, string,
 403 | 		) *Validator
 404 | 		expectValue interface{}
 405 | 		expectError bool
 406 | 	}{
 407 | 		// ValidateAndAddOptionalStringToPath tests
 408 | 		{
 409 | 			name:        "optional string to target - valid",
 410 | 			args:        map[string]interface{}{"customer_name": "Test User"},
 411 | 			paramName:   "customer_name",
 412 | 			targetKey:   "name",
 413 | 			testFunc:    (*Validator).ValidateAndAddOptionalStringToPath,
 414 | 			expectValue: "Test User",
 415 | 			expectError: false,
 416 | 		},
 417 | 		{
 418 | 			name:        "optional string to target - empty",
 419 | 			args:        map[string]interface{}{"customer_name": ""},
 420 | 			paramName:   "customer_name",
 421 | 			targetKey:   "name",
 422 | 			testFunc:    (*Validator).ValidateAndAddOptionalStringToPath,
 423 | 			expectValue: "",
 424 | 			expectError: false,
 425 | 		},
 426 | 		{
 427 | 			name:        "optional string to target - missing",
 428 | 			args:        map[string]interface{}{},
 429 | 			paramName:   "customer_name",
 430 | 			targetKey:   "name",
 431 | 			testFunc:    (*Validator).ValidateAndAddOptionalStringToPath,
 432 | 			expectValue: nil,
 433 | 			expectError: false,
 434 | 		},
 435 | 		{
 436 | 			name:        "optional string to target - wrong type",
 437 | 			args:        map[string]interface{}{"customer_name": 123},
 438 | 			paramName:   "customer_name",
 439 | 			targetKey:   "name",
 440 | 			testFunc:    (*Validator).ValidateAndAddOptionalStringToPath,
 441 | 			expectValue: nil,
 442 | 			expectError: true,
 443 | 		},
 444 | 
 445 | 		// ValidateAndAddOptionalBoolToPath tests
 446 | 		{
 447 | 			name:        "optional bool to target - true",
 448 | 			args:        map[string]interface{}{"notify_sms": true},
 449 | 			paramName:   "notify_sms",
 450 | 			targetKey:   "sms",
 451 | 			testFunc:    (*Validator).ValidateAndAddOptionalBoolToPath,
 452 | 			expectValue: true,
 453 | 			expectError: false,
 454 | 		},
 455 | 		{
 456 | 			name:        "optional bool to target - false",
 457 | 			args:        map[string]interface{}{"notify_sms": false},
 458 | 			paramName:   "notify_sms",
 459 | 			targetKey:   "sms",
 460 | 			testFunc:    (*Validator).ValidateAndAddOptionalBoolToPath,
 461 | 			expectValue: false,
 462 | 			expectError: false,
 463 | 		},
 464 | 		{
 465 | 			name:        "optional bool to target - wrong type",
 466 | 			args:        map[string]interface{}{"notify_sms": "not a bool"},
 467 | 			paramName:   "notify_sms",
 468 | 			targetKey:   "sms",
 469 | 			testFunc:    (*Validator).ValidateAndAddOptionalBoolToPath,
 470 | 			expectValue: nil,
 471 | 			expectError: true,
 472 | 		},
 473 | 
 474 | 		// ValidateAndAddOptionalIntToPath tests
 475 | 		{
 476 | 			name:        "optional int to target - valid",
 477 | 			args:        map[string]interface{}{"age": float64(25)},
 478 | 			paramName:   "age",
 479 | 			targetKey:   "customer_age",
 480 | 			testFunc:    (*Validator).ValidateAndAddOptionalIntToPath,
 481 | 			expectValue: int64(25),
 482 | 			expectError: false,
 483 | 		},
 484 | 		{
 485 | 			name:        "optional int to target - zero",
 486 | 			args:        map[string]interface{}{"age": float64(0)},
 487 | 			paramName:   "age",
 488 | 			targetKey:   "customer_age",
 489 | 			testFunc:    (*Validator).ValidateAndAddOptionalIntToPath,
 490 | 			expectValue: int64(0),
 491 | 			expectError: false,
 492 | 		},
 493 | 		{
 494 | 			name:        "optional int to target - missing",
 495 | 			args:        map[string]interface{}{},
 496 | 			paramName:   "age",
 497 | 			targetKey:   "customer_age",
 498 | 			testFunc:    (*Validator).ValidateAndAddOptionalIntToPath,
 499 | 			expectValue: nil,
 500 | 			expectError: false,
 501 | 		},
 502 | 		{
 503 | 			name:        "optional int to target - wrong type",
 504 | 			args:        map[string]interface{}{"age": "not a number"},
 505 | 			paramName:   "age",
 506 | 			targetKey:   "customer_age",
 507 | 			testFunc:    (*Validator).ValidateAndAddOptionalIntToPath,
 508 | 			expectValue: nil,
 509 | 			expectError: true,
 510 | 		},
 511 | 	}
 512 | 
 513 | 	for _, tt := range tests {
 514 | 		t.Run(tt.name, func(t *testing.T) {
 515 | 			// Create a target map for this specific test
 516 | 			target := make(map[string]interface{})
 517 | 
 518 | 			// Create the request and validator
 519 | 			request := &mcpgo.CallToolRequest{
 520 | 				Arguments: tt.args,
 521 | 			}
 522 | 			validator := NewValidator(request)
 523 | 
 524 | 			// Call the test function with target and verify its return value
 525 | 			tt.testFunc(validator, target, tt.paramName, tt.targetKey)
 526 | 
 527 | 			// Check if we got the expected errors
 528 | 			if tt.expectError {
 529 | 				assert.True(t, validator.HasErrors(), "Expected validation error")
 530 | 			} else {
 531 | 				assert.False(t, validator.HasErrors(), "Did not expect validation error")
 532 | 
 533 | 				// For non-error cases, check target map value
 534 | 				if tt.expectValue != nil {
 535 | 					// Should have the value with the target key
 536 | 					assert.Equal(t,
 537 | 						tt.expectValue,
 538 | 						target[tt.targetKey],
 539 | 						"Target map value mismatch")
 540 | 				} else {
 541 | 					// Target key should not exist
 542 | 					_, exists := target[tt.targetKey]
 543 | 					assert.False(t, exists, "Key should not be in target map when value is empty") // nolint:lll
 544 | 				}
 545 | 			}
 546 | 		})
 547 | 	}
 548 | }
 549 | 
 550 | // Test for nested validation with multiple fields into target maps
 551 | func TestValidatorNestedObjects(t *testing.T) {
 552 | 	t.Run("customer object validation", func(t *testing.T) {
 553 | 		// Create request with customer details
 554 | 		args := map[string]interface{}{
 555 | 			"customer_name":    "John Doe",
 556 | 			"customer_email":   "[email protected]",
 557 | 			"customer_contact": "+1234567890",
 558 | 		}
 559 | 		request := &mcpgo.CallToolRequest{
 560 | 			Arguments: args,
 561 | 		}
 562 | 
 563 | 		// Customer target map
 564 | 		customer := make(map[string]interface{})
 565 | 
 566 | 		// Create validator and validate customer fields
 567 | 		validator := NewValidator(request).
 568 | 			ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
 569 | 			ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
 570 | 			ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact")
 571 | 
 572 | 		// Should not have errors
 573 | 		assert.False(t, validator.HasErrors())
 574 | 
 575 | 		// Customer map should have all three fields
 576 | 		assert.Equal(t, "John Doe", customer["name"])
 577 | 		assert.Equal(t, "[email protected]", customer["email"])
 578 | 		assert.Equal(t, "+1234567890", customer["contact"])
 579 | 	})
 580 | 
 581 | 	t.Run("notification object validation", func(t *testing.T) {
 582 | 		// Create request with notification settings
 583 | 		args := map[string]interface{}{
 584 | 			"notify_sms":   true,
 585 | 			"notify_email": false,
 586 | 		}
 587 | 		request := &mcpgo.CallToolRequest{
 588 | 			Arguments: args,
 589 | 		}
 590 | 
 591 | 		// Notify target map
 592 | 		notify := make(map[string]interface{})
 593 | 
 594 | 		// Create validator and validate notification fields
 595 | 		validator := NewValidator(request).
 596 | 			ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
 597 | 			ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email")
 598 | 
 599 | 		// Should not have errors
 600 | 		assert.False(t, validator.HasErrors())
 601 | 
 602 | 		// Notify map should have both fields
 603 | 		assert.Equal(t, true, notify["sms"])
 604 | 		assert.Equal(t, false, notify["email"])
 605 | 	})
 606 | 
 607 | 	t.Run("mixed object with error", func(t *testing.T) {
 608 | 		// Create request with mixed valid and invalid data
 609 | 		args := map[string]interface{}{
 610 | 			"customer_name":  "Jane Doe",
 611 | 			"customer_email": 12345, // Wrong type
 612 | 		}
 613 | 		request := &mcpgo.CallToolRequest{
 614 | 			Arguments: args,
 615 | 		}
 616 | 
 617 | 		// Target map
 618 | 		customer := make(map[string]interface{})
 619 | 
 620 | 		// Create validator and validate fields
 621 | 		validator := NewValidator(request).
 622 | 			ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
 623 | 			ValidateAndAddOptionalStringToPath(customer, "customer_email", "email")
 624 | 
 625 | 		// Should have errors
 626 | 		assert.True(t, validator.HasErrors())
 627 | 
 628 | 		// Customer map should have only the valid field
 629 | 		assert.Equal(t, "Jane Doe", customer["name"])
 630 | 		_, hasEmail := customer["email"]
 631 | 		assert.False(t, hasEmail, "Invalid field should not be added to target map")
 632 | 	})
 633 | }
 634 | 
 635 | // Test for optional bool handling
 636 | func TestOptionalBoolBehavior(t *testing.T) {
 637 | 	t.Run("explicit bool values", func(t *testing.T) {
 638 | 		// Create request with explicit bool values
 639 | 		args := map[string]interface{}{
 640 | 			"true_param":  true,
 641 | 			"false_param": false,
 642 | 		}
 643 | 		request := &mcpgo.CallToolRequest{
 644 | 			Arguments: args,
 645 | 		}
 646 | 
 647 | 		// Create result map
 648 | 		result := make(map[string]interface{})
 649 | 
 650 | 		// Validate both parameters
 651 | 		validator := NewValidator(request).
 652 | 			ValidateAndAddOptionalBool(result, "true_param").
 653 | 			ValidateAndAddOptionalBool(result, "false_param")
 654 | 
 655 | 		// Verify no errors occurred
 656 | 		assert.False(t, validator.HasErrors())
 657 | 
 658 | 		// Both parameters should be set in the result
 659 | 		assert.Equal(t, true, result["true_param"])
 660 | 		assert.Equal(t, false, result["false_param"])
 661 | 	})
 662 | 
 663 | 	t.Run("missing bool parameter", func(t *testing.T) {
 664 | 		// Create request without bool parameters
 665 | 		args := map[string]interface{}{
 666 | 			"other_param": "some value",
 667 | 		}
 668 | 		request := &mcpgo.CallToolRequest{
 669 | 			Arguments: args,
 670 | 		}
 671 | 
 672 | 		// Create result map
 673 | 		result := make(map[string]interface{})
 674 | 
 675 | 		// Try to validate missing bool parameters
 676 | 		validator := NewValidator(request).
 677 | 			ValidateAndAddOptionalBool(result, "true_param").
 678 | 			ValidateAndAddOptionalBool(result, "false_param")
 679 | 
 680 | 		// Verify no errors occurred
 681 | 		assert.False(t, validator.HasErrors())
 682 | 
 683 | 		// Result should be empty since no bool values were provided
 684 | 		assert.Empty(t, result)
 685 | 	})
 686 | 
 687 | 	t.Run("explicit bool values with 'To' functions", func(t *testing.T) {
 688 | 		// Create request with explicit bool values
 689 | 		args := map[string]interface{}{
 690 | 			"notify_sms":   true,
 691 | 			"notify_email": false,
 692 | 		}
 693 | 		request := &mcpgo.CallToolRequest{
 694 | 			Arguments: args,
 695 | 		}
 696 | 
 697 | 		// Create target map
 698 | 		target := make(map[string]interface{})
 699 | 
 700 | 		// Validate both parameters
 701 | 		validator := NewValidator(request).
 702 | 			ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms").
 703 | 			ValidateAndAddOptionalBoolToPath(target, "notify_email", "email")
 704 | 
 705 | 		// Verify no errors occurred
 706 | 		assert.False(t, validator.HasErrors())
 707 | 
 708 | 		// Both parameters should be set in the target map
 709 | 		assert.Equal(t, true, target["sms"])
 710 | 		assert.Equal(t, false, target["email"])
 711 | 	})
 712 | 
 713 | 	t.Run("missing bool parameter with 'To' functions", func(t *testing.T) {
 714 | 		// Create request without bool parameters
 715 | 		args := map[string]interface{}{
 716 | 			"other_param": "some value",
 717 | 		}
 718 | 		request := &mcpgo.CallToolRequest{
 719 | 			Arguments: args,
 720 | 		}
 721 | 
 722 | 		// Create target map
 723 | 		target := make(map[string]interface{})
 724 | 
 725 | 		// Try to validate missing bool parameters
 726 | 		validator := NewValidator(request).
 727 | 			ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms").
 728 | 			ValidateAndAddOptionalBoolToPath(target, "notify_email", "email")
 729 | 
 730 | 		// Verify no errors occurred
 731 | 		assert.False(t, validator.HasErrors())
 732 | 
 733 | 		// Target map should be empty since no bool values were provided
 734 | 		assert.Empty(t, target)
 735 | 	})
 736 | }
 737 | 
 738 | // Test for extractValueGeneric function edge cases
 739 | func TestExtractValueGeneric(t *testing.T) {
 740 | 	t.Run("invalid arguments type", func(t *testing.T) {
 741 | 		request := &mcpgo.CallToolRequest{
 742 | 			Arguments: "invalid_type", // Not a map
 743 | 		}
 744 | 
 745 | 		result, err := extractValueGeneric[string](request, "test", false)
 746 | 		assert.Error(t, err)
 747 | 		assert.Equal(t, "invalid arguments type", err.Error())
 748 | 		assert.Nil(t, result)
 749 | 	})
 750 | 
 751 | 	t.Run("json marshal error", func(t *testing.T) {
 752 | 		// Create a value that can't be marshaled to JSON
 753 | 		args := map[string]interface{}{
 754 | 			"test_param": make(chan int), // Channels can't be marshaled
 755 | 		}
 756 | 		request := &mcpgo.CallToolRequest{
 757 | 			Arguments: args,
 758 | 		}
 759 | 
 760 | 		result, err := extractValueGeneric[string](request, "test_param", false)
 761 | 		assert.Error(t, err)
 762 | 		assert.Equal(t, "invalid parameter type: test_param", err.Error())
 763 | 		assert.Nil(t, result)
 764 | 	})
 765 | 
 766 | 	t.Run("json unmarshal error", func(t *testing.T) {
 767 | 		// Provide a value that can't be unmarshaled to the target type
 768 | 		args := map[string]interface{}{
 769 | 			"test_param": []interface{}{1, 2, 3}, // Array can't be unmarshaled to string
 770 | 		}
 771 | 		request := &mcpgo.CallToolRequest{
 772 | 			Arguments: args,
 773 | 		}
 774 | 
 775 | 		result, err := extractValueGeneric[string](request, "test_param", false)
 776 | 		assert.Error(t, err)
 777 | 		assert.Equal(t, "invalid parameter type: test_param", err.Error())
 778 | 		assert.Nil(t, result)
 779 | 	})
 780 | }
 781 | 
 782 | // Test for validateAndAddRequired function
 783 | func TestValidateAndAddRequired(t *testing.T) {
 784 | 	t.Run("successful validation", func(t *testing.T) {
 785 | 		args := map[string]interface{}{
 786 | 			"test_param": "test_value",
 787 | 		}
 788 | 		request := &mcpgo.CallToolRequest{
 789 | 			Arguments: args,
 790 | 		}
 791 | 
 792 | 		params := make(map[string]interface{})
 793 | 		validator := NewValidator(request)
 794 | 
 795 | 		result := validateAndAddRequired[string](validator, params, "test_param")
 796 | 
 797 | 		assert.False(t, result.HasErrors())
 798 | 		assert.Equal(t, "test_value", params["test_param"])
 799 | 	})
 800 | 
 801 | 	t.Run("validation error", func(t *testing.T) {
 802 | 		request := &mcpgo.CallToolRequest{
 803 | 			Arguments: "invalid_type",
 804 | 		}
 805 | 
 806 | 		params := make(map[string]interface{})
 807 | 		validator := NewValidator(request)
 808 | 
 809 | 		result := validateAndAddRequired[string](validator, params, "test_param")
 810 | 
 811 | 		assert.True(t, result.HasErrors())
 812 | 		assert.Empty(t, params)
 813 | 	})
 814 | 
 815 | 	t.Run("nil value after successful extraction", func(t *testing.T) {
 816 | 		// This edge case is hard to trigger directly, but we can simulate it
 817 | 		// by using a type that extractValueGeneric might return as nil
 818 | 		args := map[string]interface{}{
 819 | 			"test_param": nil,
 820 | 		}
 821 | 		request := &mcpgo.CallToolRequest{
 822 | 			Arguments: args,
 823 | 		}
 824 | 
 825 | 		params := make(map[string]interface{})
 826 | 		validator := NewValidator(request)
 827 | 
 828 | 		result := validateAndAddRequired[string](validator, params, "test_param")
 829 | 
 830 | 		// This should result in an error because the parameter is required
 831 | 		assert.True(t, result.HasErrors())
 832 | 		assert.Empty(t, params)
 833 | 	})
 834 | }
 835 | 
 836 | // Test for validateAndAddOptional function
 837 | func TestValidateAndAddOptional(t *testing.T) {
 838 | 	t.Run("successful validation", func(t *testing.T) {
 839 | 		args := map[string]interface{}{
 840 | 			"test_param": "test_value",
 841 | 		}
 842 | 		request := &mcpgo.CallToolRequest{
 843 | 			Arguments: args,
 844 | 		}
 845 | 
 846 | 		params := make(map[string]interface{})
 847 | 		validator := NewValidator(request)
 848 | 
 849 | 		result := validateAndAddOptional[string](validator, params, "test_param")
 850 | 
 851 | 		assert.False(t, result.HasErrors())
 852 | 		assert.Equal(t, "test_value", params["test_param"])
 853 | 	})
 854 | 
 855 | 	t.Run("validation error", func(t *testing.T) {
 856 | 		request := &mcpgo.CallToolRequest{
 857 | 			Arguments: "invalid_type",
 858 | 		}
 859 | 
 860 | 		params := make(map[string]interface{})
 861 | 		validator := NewValidator(request)
 862 | 
 863 | 		result := validateAndAddOptional[string](validator, params, "test_param")
 864 | 
 865 | 		assert.True(t, result.HasErrors())
 866 | 		assert.Empty(t, params)
 867 | 	})
 868 | 
 869 | 	t.Run("nil value handling", func(t *testing.T) {
 870 | 		args := map[string]interface{}{
 871 | 			"test_param": nil,
 872 | 		}
 873 | 		request := &mcpgo.CallToolRequest{
 874 | 			Arguments: args,
 875 | 		}
 876 | 
 877 | 		params := make(map[string]interface{})
 878 | 		validator := NewValidator(request)
 879 | 
 880 | 		result := validateAndAddOptional[string](validator, params, "test_param")
 881 | 
 882 | 		assert.False(t, result.HasErrors())
 883 | 		assert.Empty(t, params)
 884 | 	})
 885 | }
 886 | 
 887 | // Test for validateAndAddToPath function
 888 | func TestValidateAndAddToPath(t *testing.T) {
 889 | 	t.Run("successful validation", func(t *testing.T) {
 890 | 		args := map[string]interface{}{
 891 | 			"test_param": "test_value",
 892 | 		}
 893 | 		request := &mcpgo.CallToolRequest{
 894 | 			Arguments: args,
 895 | 		}
 896 | 
 897 | 		target := make(map[string]interface{})
 898 | 		validator := NewValidator(request)
 899 | 
 900 | 		result := validateAndAddToPath[string](
 901 | 			validator, target, "test_param", "target_key")
 902 | 
 903 | 		assert.False(t, result.HasErrors())
 904 | 		assert.Equal(t, "test_value", target["target_key"])
 905 | 	})
 906 | 
 907 | 	t.Run("validation error", func(t *testing.T) {
 908 | 		request := &mcpgo.CallToolRequest{
 909 | 			Arguments: "invalid_type",
 910 | 		}
 911 | 
 912 | 		target := make(map[string]interface{})
 913 | 		validator := NewValidator(request)
 914 | 
 915 | 		result := validateAndAddToPath[string](
 916 | 			validator, target, "test_param", "target_key")
 917 | 
 918 | 		assert.True(t, result.HasErrors())
 919 | 		assert.Empty(t, target)
 920 | 	})
 921 | 
 922 | 	t.Run("nil value handling", func(t *testing.T) {
 923 | 		args := map[string]interface{}{
 924 | 			"test_param": nil,
 925 | 		}
 926 | 		request := &mcpgo.CallToolRequest{
 927 | 			Arguments: args,
 928 | 		}
 929 | 
 930 | 		target := make(map[string]interface{})
 931 | 		validator := NewValidator(request)
 932 | 
 933 | 		result := validateAndAddToPath[string](
 934 | 			validator, target, "test_param", "target_key")
 935 | 
 936 | 		assert.False(t, result.HasErrors())
 937 | 		assert.Empty(t, target)
 938 | 	})
 939 | }
 940 | 
 941 | // Test for ValidateAndAddPagination function
 942 | func TestValidateAndAddPagination(t *testing.T) {
 943 | 	t.Run("all pagination parameters", func(t *testing.T) {
 944 | 		args := map[string]interface{}{
 945 | 			"count": 10,
 946 | 			"skip":  5,
 947 | 		}
 948 | 		request := &mcpgo.CallToolRequest{
 949 | 			Arguments: args,
 950 | 		}
 951 | 
 952 | 		params := make(map[string]interface{})
 953 | 		validator := NewValidator(request).ValidateAndAddPagination(params)
 954 | 
 955 | 		assert.False(t, validator.HasErrors())
 956 | 		assert.Equal(t, int64(10), params["count"])
 957 | 		assert.Equal(t, int64(5), params["skip"])
 958 | 	})
 959 | 
 960 | 	t.Run("missing pagination parameters", func(t *testing.T) {
 961 | 		args := map[string]interface{}{}
 962 | 		request := &mcpgo.CallToolRequest{
 963 | 			Arguments: args,
 964 | 		}
 965 | 
 966 | 		params := make(map[string]interface{})
 967 | 		validator := NewValidator(request).ValidateAndAddPagination(params)
 968 | 
 969 | 		assert.False(t, validator.HasErrors())
 970 | 		assert.Empty(t, params)
 971 | 	})
 972 | 
 973 | 	t.Run("invalid count type", func(t *testing.T) {
 974 | 		args := map[string]interface{}{
 975 | 			"count": "invalid",
 976 | 		}
 977 | 		request := &mcpgo.CallToolRequest{
 978 | 			Arguments: args,
 979 | 		}
 980 | 
 981 | 		params := make(map[string]interface{})
 982 | 		validator := NewValidator(request).ValidateAndAddPagination(params)
 983 | 
 984 | 		assert.True(t, validator.HasErrors())
 985 | 	})
 986 | }
 987 | 
 988 | // Test for ValidateAndAddExpand function
 989 | func TestValidateAndAddExpand(t *testing.T) {
 990 | 	t.Run("valid expand parameter", func(t *testing.T) {
 991 | 		args := map[string]interface{}{
 992 | 			"expand": []string{"payments", "customer"},
 993 | 		}
 994 | 		request := &mcpgo.CallToolRequest{
 995 | 			Arguments: args,
 996 | 		}
 997 | 
 998 | 		params := make(map[string]interface{})
 999 | 		validator := NewValidator(request).ValidateAndAddExpand(params)
1000 | 
1001 | 		assert.False(t, validator.HasErrors())
1002 | 		// The function sets expand[] for each value, so check the last one
1003 | 		assert.Equal(t, "customer", params["expand[]"])
1004 | 	})
1005 | 
1006 | 	t.Run("missing expand parameter", func(t *testing.T) {
1007 | 		args := map[string]interface{}{}
1008 | 		request := &mcpgo.CallToolRequest{
1009 | 			Arguments: args,
1010 | 		}
1011 | 
1012 | 		params := make(map[string]interface{})
1013 | 		validator := NewValidator(request).ValidateAndAddExpand(params)
1014 | 
1015 | 		assert.False(t, validator.HasErrors())
1016 | 		assert.Empty(t, params)
1017 | 	})
1018 | 
1019 | 	t.Run("invalid expand type", func(t *testing.T) {
1020 | 		args := map[string]interface{}{
1021 | 			"expand": "invalid", // Should be []string, not string
1022 | 		}
1023 | 		request := &mcpgo.CallToolRequest{
1024 | 			Arguments: args,
1025 | 		}
1026 | 
1027 | 		params := make(map[string]interface{})
1028 | 		validator := NewValidator(request).ValidateAndAddExpand(params)
1029 | 
1030 | 		assert.True(t, validator.HasErrors())
1031 | 	})
1032 | }
1033 | 
1034 | // Test for token validation functions edge cases
1035 | func TestTokenValidationEdgeCases(t *testing.T) {
1036 | 	t.Run("validateTokenMaxAmount - int conversion", func(t *testing.T) {
1037 | 		token := map[string]interface{}{
1038 | 			"max_amount": 100, // int instead of float64
1039 | 		}
1040 | 
1041 | 		request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
1042 | 		validator := NewValidator(request).validateTokenMaxAmount(token)
1043 | 
1044 | 		assert.False(t, validator.HasErrors())
1045 | 		assert.Equal(t, float64(100), token["max_amount"])
1046 | 	})
1047 | 
1048 | 	t.Run("validateTokenExpireAt - int conversion", func(t *testing.T) {
1049 | 		token := map[string]interface{}{
1050 | 			"expire_at": 1234567890, // int instead of float64
1051 | 		}
1052 | 
1053 | 		request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
1054 | 		validator := NewValidator(request).validateTokenExpireAt(token)
1055 | 
1056 | 		assert.False(t, validator.HasErrors())
1057 | 		assert.Equal(t, float64(1234567890), token["expire_at"])
1058 | 	})
1059 | 
1060 | 	t.Run("validateTokenExpireAt - zero value", func(t *testing.T) {
1061 | 		token := map[string]interface{}{
1062 | 			"expire_at": 0,
1063 | 		}
1064 | 
1065 | 		request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
1066 | 		validator := NewValidator(request).validateTokenExpireAt(token)
1067 | 
1068 | 		assert.True(t, validator.HasErrors())
1069 | 	})
1070 | 
1071 | 	t.Run("validateTokenMaxAmount - zero value", func(t *testing.T) {
1072 | 		token := map[string]interface{}{
1073 | 			"max_amount": 0,
1074 | 		}
1075 | 
1076 | 		request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
1077 | 		validator := NewValidator(request).validateTokenMaxAmount(token)
1078 | 
1079 | 		assert.True(t, validator.HasErrors())
1080 | 	})
1081 | }
1082 | 
1083 | // Test for ValidateAndAddToken edge cases
1084 | func TestValidateAndAddTokenEdgeCases(t *testing.T) {
1085 | 	t.Run("token extraction error", func(t *testing.T) {
1086 | 		request := &mcpgo.CallToolRequest{
1087 | 			Arguments: "invalid_type",
1088 | 		}
1089 | 
1090 | 		params := make(map[string]interface{})
1091 | 		validator := NewValidator(request).ValidateAndAddToken(params, "token")
1092 | 
1093 | 		assert.True(t, validator.HasErrors())
1094 | 		assert.Empty(t, params)
1095 | 	})
1096 | 
1097 | 	t.Run("nil token value", func(t *testing.T) {
1098 | 		args := map[string]interface{}{
1099 | 			"token": nil,
1100 | 		}
1101 | 		request := &mcpgo.CallToolRequest{
1102 | 			Arguments: args,
1103 | 		}
1104 | 
1105 | 		params := make(map[string]interface{})
1106 | 		validator := NewValidator(request).ValidateAndAddToken(params, "token")
1107 | 
1108 | 		assert.False(t, validator.HasErrors())
1109 | 		assert.Empty(t, params)
1110 | 	})
1111 | 
1112 | 	t.Run("token validation errors", func(t *testing.T) {
1113 | 		args := map[string]interface{}{
1114 | 			"token": map[string]interface{}{
1115 | 				"max_amount": -100, // Invalid value
1116 | 			},
1117 | 		}
1118 | 		request := &mcpgo.CallToolRequest{
1119 | 			Arguments: args,
1120 | 		}
1121 | 
1122 | 		params := make(map[string]interface{})
1123 | 		validator := NewValidator(request).ValidateAndAddToken(params, "token")
1124 | 
1125 | 		assert.True(t, validator.HasErrors())
1126 | 		assert.Empty(t, params)
1127 | 	})
1128 | }
1129 | 
```
Page 4/5FirstPrevNextLast