#
tokens: 46489/50000 6/69 files (page 3/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 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/payment_links_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_CreatePaymentLink(t *testing.T) {
 15 | 	createPaymentLinkPath := fmt.Sprintf(
 16 | 		"/%s%s",
 17 | 		constants.VERSION_V1,
 18 | 		constants.PaymentLink_URL,
 19 | 	)
 20 | 
 21 | 	successfulPaymentLinkResp := map[string]interface{}{
 22 | 		"id":          "plink_ExjpAUN3gVHrPJ",
 23 | 		"amount":      float64(50000),
 24 | 		"currency":    "INR",
 25 | 		"description": "Test payment",
 26 | 		"status":      "created",
 27 | 		"short_url":   "https://rzp.io/i/nxrHnLJ",
 28 | 	}
 29 | 
 30 | 	paymentLinkWithoutDescResp := map[string]interface{}{
 31 | 		"id":        "plink_ExjpAUN3gVHrPJ",
 32 | 		"amount":    float64(50000),
 33 | 		"currency":  "INR",
 34 | 		"status":    "created",
 35 | 		"short_url": "https://rzp.io/i/nxrHnLJ",
 36 | 	}
 37 | 
 38 | 	invalidCurrencyErrorResp := map[string]interface{}{
 39 | 		"error": map[string]interface{}{
 40 | 			"code":        "BAD_REQUEST_ERROR",
 41 | 			"description": "API error: Invalid currency",
 42 | 		},
 43 | 	}
 44 | 
 45 | 	tests := []RazorpayToolTestCase{
 46 | 		{
 47 | 			Name: "successful payment link creation",
 48 | 			Request: map[string]interface{}{
 49 | 				"amount":      float64(50000),
 50 | 				"currency":    "INR",
 51 | 				"description": "Test payment",
 52 | 			},
 53 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 54 | 				return mock.NewHTTPClient(
 55 | 					mock.Endpoint{
 56 | 						Path:     createPaymentLinkPath,
 57 | 						Method:   "POST",
 58 | 						Response: successfulPaymentLinkResp,
 59 | 					},
 60 | 				)
 61 | 			},
 62 | 			ExpectError:    false,
 63 | 			ExpectedResult: successfulPaymentLinkResp,
 64 | 		},
 65 | 		{
 66 | 			Name: "payment link without description",
 67 | 			Request: map[string]interface{}{
 68 | 				"amount":   float64(50000),
 69 | 				"currency": "INR",
 70 | 			},
 71 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 72 | 				return mock.NewHTTPClient(
 73 | 					mock.Endpoint{
 74 | 						Path:     createPaymentLinkPath,
 75 | 						Method:   "POST",
 76 | 						Response: paymentLinkWithoutDescResp,
 77 | 					},
 78 | 				)
 79 | 			},
 80 | 			ExpectError:    false,
 81 | 			ExpectedResult: paymentLinkWithoutDescResp,
 82 | 		},
 83 | 		{
 84 | 			Name: "missing amount parameter",
 85 | 			Request: map[string]interface{}{
 86 | 				"currency": "INR",
 87 | 			},
 88 | 			MockHttpClient: nil, // No HTTP client needed for validation error
 89 | 			ExpectError:    true,
 90 | 			ExpectedErrMsg: "missing required parameter: amount",
 91 | 		},
 92 | 		{
 93 | 			Name: "missing currency parameter",
 94 | 			Request: map[string]interface{}{
 95 | 				"amount": float64(50000),
 96 | 			},
 97 | 			MockHttpClient: nil, // No HTTP client needed for validation error
 98 | 			ExpectError:    true,
 99 | 			ExpectedErrMsg: "missing required parameter: currency",
100 | 		},
101 | 		{
102 | 			Name: "multiple validation errors",
103 | 			Request: map[string]interface{}{
104 | 				// Missing both amount and currency (required parameters)
105 | 				"description": 12345, // Wrong type for description
106 | 			},
107 | 			MockHttpClient: nil, // No HTTP client needed for validation error
108 | 			ExpectError:    true,
109 | 			ExpectedErrMsg: "Validation errors:\n- " +
110 | 				"missing required parameter: amount\n- " +
111 | 				"missing required parameter: currency\n- " +
112 | 				"invalid parameter type: description",
113 | 		},
114 | 		{
115 | 			Name: "payment link creation fails",
116 | 			Request: map[string]interface{}{
117 | 				"amount":   float64(50000),
118 | 				"currency": "XYZ", // Invalid currency
119 | 			},
120 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
121 | 				return mock.NewHTTPClient(
122 | 					mock.Endpoint{
123 | 						Path:     createPaymentLinkPath,
124 | 						Method:   "POST",
125 | 						Response: invalidCurrencyErrorResp,
126 | 					},
127 | 				)
128 | 			},
129 | 			ExpectError:    true,
130 | 			ExpectedErrMsg: "creating payment link failed: API error: Invalid currency",
131 | 		},
132 | 	}
133 | 
134 | 	for _, tc := range tests {
135 | 		t.Run(tc.Name, func(t *testing.T) {
136 | 			runToolTest(t, tc, CreatePaymentLink, "Payment Link")
137 | 		})
138 | 	}
139 | }
140 | 
141 | func Test_FetchPaymentLink(t *testing.T) {
142 | 	fetchPaymentLinkPathFmt := fmt.Sprintf(
143 | 		"/%s%s/%%s",
144 | 		constants.VERSION_V1,
145 | 		constants.PaymentLink_URL,
146 | 	)
147 | 
148 | 	// Define common response maps to be reused
149 | 	paymentLinkResp := map[string]interface{}{
150 | 		"id":          "plink_ExjpAUN3gVHrPJ",
151 | 		"amount":      float64(50000),
152 | 		"currency":    "INR",
153 | 		"description": "Test payment",
154 | 		"status":      "paid",
155 | 		"short_url":   "https://rzp.io/i/nxrHnLJ",
156 | 	}
157 | 
158 | 	paymentLinkNotFoundResp := map[string]interface{}{
159 | 		"error": map[string]interface{}{
160 | 			"code":        "BAD_REQUEST_ERROR",
161 | 			"description": "payment link not found",
162 | 		},
163 | 	}
164 | 
165 | 	tests := []RazorpayToolTestCase{
166 | 		{
167 | 			Name: "successful payment link fetch",
168 | 			Request: map[string]interface{}{
169 | 				"payment_link_id": "plink_ExjpAUN3gVHrPJ",
170 | 			},
171 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
172 | 				return mock.NewHTTPClient(
173 | 					mock.Endpoint{
174 | 						Path:     fmt.Sprintf(fetchPaymentLinkPathFmt, "plink_ExjpAUN3gVHrPJ"),
175 | 						Method:   "GET",
176 | 						Response: paymentLinkResp,
177 | 					},
178 | 				)
179 | 			},
180 | 			ExpectError:    false,
181 | 			ExpectedResult: paymentLinkResp,
182 | 		},
183 | 		{
184 | 			Name: "payment link not found",
185 | 			Request: map[string]interface{}{
186 | 				"payment_link_id": "plink_invalid",
187 | 			},
188 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
189 | 				return mock.NewHTTPClient(
190 | 					mock.Endpoint{
191 | 						Path:     fmt.Sprintf(fetchPaymentLinkPathFmt, "plink_invalid"),
192 | 						Method:   "GET",
193 | 						Response: paymentLinkNotFoundResp,
194 | 					},
195 | 				)
196 | 			},
197 | 			ExpectError:    true,
198 | 			ExpectedErrMsg: "fetching payment link failed: payment link not found",
199 | 		},
200 | 		{
201 | 			Name:           "missing payment_link_id parameter",
202 | 			Request:        map[string]interface{}{},
203 | 			MockHttpClient: nil, // No HTTP client needed for validation error
204 | 			ExpectError:    true,
205 | 			ExpectedErrMsg: "missing required parameter: payment_link_id",
206 | 		},
207 | 		{
208 | 			Name: "multiple validation errors",
209 | 			Request: map[string]interface{}{
210 | 				// Missing payment_link_id parameter
211 | 				"non_existent_param": 12345, // Additional parameter that doesn't exist
212 | 			},
213 | 			MockHttpClient: nil, // No HTTP client needed for validation error
214 | 			ExpectError:    true,
215 | 			ExpectedErrMsg: "missing required parameter: payment_link_id",
216 | 		},
217 | 	}
218 | 
219 | 	for _, tc := range tests {
220 | 		t.Run(tc.Name, func(t *testing.T) {
221 | 			runToolTest(t, tc, FetchPaymentLink, "Payment Link")
222 | 		})
223 | 	}
224 | }
225 | 
226 | func Test_CreateUpiPaymentLink(t *testing.T) {
227 | 	createPaymentLinkPath := fmt.Sprintf(
228 | 		"/%s%s",
229 | 		constants.VERSION_V1,
230 | 		constants.PaymentLink_URL,
231 | 	)
232 | 
233 | 	upiPaymentLinkWithAllParamsResp := map[string]interface{}{
234 | 		"id":              "plink_UpiAllParamsExjpAUN3gVHrPJ",
235 | 		"amount":          float64(50000),
236 | 		"currency":        "INR",
237 | 		"description":     "Test UPI payment with all params",
238 | 		"reference_id":    "REF12345",
239 | 		"accept_partial":  true,
240 | 		"expire_by":       float64(1718196584),
241 | 		"reminder_enable": true,
242 | 		"status":          "created",
243 | 		"short_url":       "https://rzp.io/i/upiAllParams123",
244 | 		"upi_link":        true,
245 | 		"customer": map[string]interface{}{
246 | 			"name":    "Test Customer",
247 | 			"email":   "[email protected]",
248 | 			"contact": "+919876543210",
249 | 		},
250 | 		"notes": map[string]interface{}{
251 | 			"policy_name": "Test Policy",
252 | 			"user_id":     "usr_123",
253 | 		},
254 | 	}
255 | 
256 | 	errorResp := map[string]interface{}{
257 | 		"error": map[string]interface{}{
258 | 			"code":        "BAD_REQUEST_ERROR",
259 | 			"description": "API error: Something went wrong",
260 | 		},
261 | 	}
262 | 
263 | 	tests := []RazorpayToolTestCase{
264 | 		{
265 | 			Name: "UPI payment link with all parameters",
266 | 			Request: map[string]interface{}{
267 | 				"amount":                   float64(50000),
268 | 				"currency":                 "INR",
269 | 				"description":              "Test UPI payment with all params",
270 | 				"reference_id":             "REF12345",
271 | 				"accept_partial":           true,
272 | 				"first_min_partial_amount": float64(10000),
273 | 				"expire_by":                float64(1718196584),
274 | 				"customer_name":            "Test Customer",
275 | 				"customer_email":           "[email protected]",
276 | 				"customer_contact":         "+919876543210",
277 | 				"notify_sms":               true,
278 | 				"notify_email":             true,
279 | 				"reminder_enable":          true,
280 | 				"notes": map[string]interface{}{
281 | 					"policy_name": "Test Policy",
282 | 					"user_id":     "usr_123",
283 | 				},
284 | 				"callback_url":    "https://example.com/callback",
285 | 				"callback_method": "get",
286 | 			},
287 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
288 | 				return mock.NewHTTPClient(
289 | 					mock.Endpoint{
290 | 						Path:     createPaymentLinkPath,
291 | 						Method:   "POST",
292 | 						Response: upiPaymentLinkWithAllParamsResp,
293 | 					},
294 | 				)
295 | 			},
296 | 			ExpectError:    false,
297 | 			ExpectedResult: upiPaymentLinkWithAllParamsResp,
298 | 		},
299 | 		{
300 | 			Name:           "missing amount parameter",
301 | 			Request:        map[string]interface{}{},
302 | 			MockHttpClient: nil, // No HTTP client needed for validation error
303 | 			ExpectError:    true,
304 | 			ExpectedErrMsg: "missing required parameter: amount",
305 | 		},
306 | 		{
307 | 			Name: "UPI payment link creation fails",
308 | 			Request: map[string]interface{}{
309 | 				"amount": float64(50000),
310 | 			},
311 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
312 | 				return mock.NewHTTPClient(
313 | 					mock.Endpoint{
314 | 						Path:     createPaymentLinkPath,
315 | 						Method:   "POST",
316 | 						Response: errorResp,
317 | 					},
318 | 				)
319 | 			},
320 | 			ExpectError:    true,
321 | 			ExpectedErrMsg: "missing required parameter: currency",
322 | 		},
323 | 	}
324 | 
325 | 	for _, tc := range tests {
326 | 		t.Run(tc.Name, func(t *testing.T) {
327 | 			runToolTest(t, tc, CreateUpiPaymentLink, "UPI Payment Link")
328 | 		})
329 | 	}
330 | }
331 | 
332 | func Test_ResendPaymentLinkNotification(t *testing.T) {
333 | 	notifyPaymentLinkPathFmt := fmt.Sprintf(
334 | 		"/%s%s/%%s/notify_by/%%s",
335 | 		constants.VERSION_V1,
336 | 		constants.PaymentLink_URL,
337 | 	)
338 | 
339 | 	successResponse := map[string]interface{}{
340 | 		"success": true,
341 | 	}
342 | 
343 | 	invalidMediumErrorResp := map[string]interface{}{
344 | 		"error": map[string]interface{}{
345 | 			"code":        "BAD_REQUEST_ERROR",
346 | 			"description": "not a valid notification medium",
347 | 		},
348 | 	}
349 | 
350 | 	tests := []RazorpayToolTestCase{
351 | 		{
352 | 			Name: "successful SMS notification",
353 | 			Request: map[string]interface{}{
354 | 				"payment_link_id": "plink_ExjpAUN3gVHrPJ",
355 | 				"medium":          "sms",
356 | 			},
357 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
358 | 				return mock.NewHTTPClient(
359 | 					mock.Endpoint{
360 | 						Path: fmt.Sprintf(
361 | 							notifyPaymentLinkPathFmt,
362 | 							"plink_ExjpAUN3gVHrPJ",
363 | 							"sms",
364 | 						),
365 | 						Method:   "POST",
366 | 						Response: successResponse,
367 | 					},
368 | 				)
369 | 			},
370 | 			ExpectError:    false,
371 | 			ExpectedResult: successResponse,
372 | 		},
373 | 		{
374 | 			Name: "missing payment_link_id parameter",
375 | 			Request: map[string]interface{}{
376 | 				"medium": "sms",
377 | 			},
378 | 			MockHttpClient: nil, // No HTTP client needed for validation error
379 | 			ExpectError:    true,
380 | 			ExpectedErrMsg: "missing required parameter: payment_link_id",
381 | 		},
382 | 		{
383 | 			Name: "missing medium parameter",
384 | 			Request: map[string]interface{}{
385 | 				"payment_link_id": "plink_ExjpAUN3gVHrPJ",
386 | 			},
387 | 			MockHttpClient: nil, // No HTTP client needed for validation error
388 | 			ExpectError:    true,
389 | 			ExpectedErrMsg: "missing required parameter: medium",
390 | 		},
391 | 		{
392 | 			Name: "API error response",
393 | 			Request: map[string]interface{}{
394 | 				"payment_link_id": "plink_Invalid",
395 | 				"medium":          "sms", // Using valid medium so it passes validation
396 | 			},
397 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
398 | 				return mock.NewHTTPClient(
399 | 					mock.Endpoint{
400 | 						Path: fmt.Sprintf(
401 | 							notifyPaymentLinkPathFmt,
402 | 							"plink_Invalid",
403 | 							"sms",
404 | 						),
405 | 						Method:   "POST",
406 | 						Response: invalidMediumErrorResp,
407 | 					},
408 | 				)
409 | 			},
410 | 			ExpectError: true,
411 | 			ExpectedErrMsg: "sending notification failed: " +
412 | 				"not a valid notification medium",
413 | 		},
414 | 	}
415 | 
416 | 	for _, tc := range tests {
417 | 		t.Run(tc.Name, func(t *testing.T) {
418 | 			toolFunc := ResendPaymentLinkNotification
419 | 			runToolTest(t, tc, toolFunc, "Payment Link Notification")
420 | 		})
421 | 	}
422 | }
423 | 
424 | func Test_UpdatePaymentLink(t *testing.T) {
425 | 	updatePaymentLinkPathFmt := fmt.Sprintf(
426 | 		"/%s%s/%%s",
427 | 		constants.VERSION_V1,
428 | 		constants.PaymentLink_URL,
429 | 	)
430 | 
431 | 	updatedPaymentLinkResp := map[string]interface{}{
432 | 		"id":              "plink_FL5HCrWEO112OW",
433 | 		"amount":          float64(1000),
434 | 		"currency":        "INR",
435 | 		"status":          "created",
436 | 		"reference_id":    "TS35",
437 | 		"expire_by":       float64(1612092283),
438 | 		"reminder_enable": false,
439 | 		"notes": []interface{}{
440 | 			map[string]interface{}{
441 | 				"key":   "policy_name",
442 | 				"value": "Jeevan Saral",
443 | 			},
444 | 		},
445 | 	}
446 | 
447 | 	invalidStateResp := map[string]interface{}{
448 | 		"error": map[string]interface{}{
449 | 			"code":        "BAD_REQUEST_ERROR",
450 | 			"description": "update can only be made in created or partially paid state",
451 | 		},
452 | 	}
453 | 
454 | 	tests := []RazorpayToolTestCase{
455 | 		{
456 | 			Name: "successful update with multiple fields",
457 | 			Request: map[string]interface{}{
458 | 				"payment_link_id": "plink_FL5HCrWEO112OW",
459 | 				"reference_id":    "TS35",
460 | 				"expire_by":       float64(1612092283),
461 | 				"reminder_enable": false,
462 | 				"accept_partial":  true,
463 | 				"notes": map[string]interface{}{
464 | 					"policy_name": "Jeevan Saral",
465 | 				},
466 | 			},
467 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
468 | 				return mock.NewHTTPClient(
469 | 					mock.Endpoint{
470 | 						Path: fmt.Sprintf(
471 | 							updatePaymentLinkPathFmt,
472 | 							"plink_FL5HCrWEO112OW",
473 | 						),
474 | 						Method:   "PATCH",
475 | 						Response: updatedPaymentLinkResp,
476 | 					},
477 | 				)
478 | 			},
479 | 			ExpectError:    false,
480 | 			ExpectedResult: updatedPaymentLinkResp,
481 | 		},
482 | 		{
483 | 			Name: "successful update with single field",
484 | 			Request: map[string]interface{}{
485 | 				"payment_link_id": "plink_FL5HCrWEO112OW",
486 | 				"reference_id":    "TS35",
487 | 			},
488 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
489 | 				return mock.NewHTTPClient(
490 | 					mock.Endpoint{
491 | 						Path: fmt.Sprintf(
492 | 							updatePaymentLinkPathFmt,
493 | 							"plink_FL5HCrWEO112OW",
494 | 						),
495 | 						Method:   "PATCH",
496 | 						Response: updatedPaymentLinkResp,
497 | 					},
498 | 				)
499 | 			},
500 | 			ExpectError:    false,
501 | 			ExpectedResult: updatedPaymentLinkResp,
502 | 		},
503 | 		{
504 | 			Name: "missing payment_link_id parameter",
505 | 			Request: map[string]interface{}{
506 | 				"reference_id": "TS35",
507 | 			},
508 | 			MockHttpClient: nil, // No HTTP client needed for validation error
509 | 			ExpectError:    true,
510 | 			ExpectedErrMsg: "missing required parameter: payment_link_id",
511 | 		},
512 | 		{
513 | 			Name: "no update fields provided",
514 | 			Request: map[string]interface{}{
515 | 				"payment_link_id": "plink_FL5HCrWEO112OW",
516 | 			},
517 | 			MockHttpClient: nil, // No HTTP client needed for validation error
518 | 			ExpectError:    true,
519 | 			ExpectedErrMsg: "at least one field to update must be provided",
520 | 		},
521 | 		{
522 | 			Name: "payment link in invalid state",
523 | 			Request: map[string]interface{}{
524 | 				"payment_link_id": "plink_Paid",
525 | 				"reference_id":    "TS35",
526 | 			},
527 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
528 | 				return mock.NewHTTPClient(
529 | 					mock.Endpoint{
530 | 						Path: fmt.Sprintf(
531 | 							updatePaymentLinkPathFmt,
532 | 							"plink_Paid",
533 | 						),
534 | 						Method:   "PATCH",
535 | 						Response: invalidStateResp,
536 | 					},
537 | 				)
538 | 			},
539 | 			ExpectError: true,
540 | 			ExpectedErrMsg: "updating payment link failed: update can only be made in " +
541 | 				"created or partially paid state",
542 | 		},
543 | 		{
544 | 			Name: "update with explicit false value",
545 | 			Request: map[string]interface{}{
546 | 				"payment_link_id": "plink_FL5HCrWEO112OW",
547 | 				"reminder_enable": false, // Explicitly set to false
548 | 			},
549 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
550 | 				return mock.NewHTTPClient(
551 | 					mock.Endpoint{
552 | 						Path: fmt.Sprintf(
553 | 							updatePaymentLinkPathFmt,
554 | 							"plink_FL5HCrWEO112OW",
555 | 						),
556 | 						Method:   "PATCH",
557 | 						Response: updatedPaymentLinkResp,
558 | 					},
559 | 				)
560 | 			},
561 | 			ExpectError:    false,
562 | 			ExpectedResult: updatedPaymentLinkResp,
563 | 		},
564 | 	}
565 | 
566 | 	for _, tc := range tests {
567 | 		t.Run(tc.Name, func(t *testing.T) {
568 | 			toolFunc := UpdatePaymentLink
569 | 			runToolTest(t, tc, toolFunc, "Payment Link Update")
570 | 		})
571 | 	}
572 | }
573 | 
574 | func Test_FetchAllPaymentLinks(t *testing.T) {
575 | 	fetchAllPaymentLinksPath := fmt.Sprintf(
576 | 		"/%s%s",
577 | 		constants.VERSION_V1,
578 | 		constants.PaymentLink_URL,
579 | 	)
580 | 
581 | 	allPaymentLinksResp := map[string]interface{}{
582 | 		"payment_links": []interface{}{
583 | 			map[string]interface{}{
584 | 				"id":           "plink_KBnb7I424Rc1R9",
585 | 				"amount":       float64(10000),
586 | 				"currency":     "INR",
587 | 				"status":       "paid",
588 | 				"description":  "Grocery",
589 | 				"reference_id": "111",
590 | 				"short_url":    "https://rzp.io/i/alaBxs0i",
591 | 				"upi_link":     false,
592 | 			},
593 | 			map[string]interface{}{
594 | 				"id":           "plink_JP6yOUDCuHgcrl",
595 | 				"amount":       float64(10000),
596 | 				"currency":     "INR",
597 | 				"status":       "paid",
598 | 				"description":  "Online Tutoring - 1 Month",
599 | 				"reference_id": "11212",
600 | 				"short_url":    "https://rzp.io/i/0ioYuawFu",
601 | 				"upi_link":     false,
602 | 			},
603 | 		},
604 | 	}
605 | 
606 | 	errorResp := map[string]interface{}{
607 | 		"error": map[string]interface{}{
608 | 			"code":        "BAD_REQUEST_ERROR",
609 | 			"description": "The api key/secret provided is invalid",
610 | 		},
611 | 	}
612 | 
613 | 	tests := []RazorpayToolTestCase{
614 | 		{
615 | 			Name:    "fetch all payment links",
616 | 			Request: map[string]interface{}{},
617 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
618 | 				return mock.NewHTTPClient(
619 | 					mock.Endpoint{
620 | 						Path:     fetchAllPaymentLinksPath,
621 | 						Method:   "GET",
622 | 						Response: allPaymentLinksResp,
623 | 					},
624 | 				)
625 | 			},
626 | 			ExpectError:    false,
627 | 			ExpectedResult: allPaymentLinksResp,
628 | 		},
629 | 		{
630 | 			Name:    "api error",
631 | 			Request: map[string]interface{}{},
632 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
633 | 				return mock.NewHTTPClient(
634 | 					mock.Endpoint{
635 | 						Path:     fetchAllPaymentLinksPath,
636 | 						Method:   "GET",
637 | 						Response: errorResp,
638 | 					},
639 | 				)
640 | 			},
641 | 			ExpectError:    true,
642 | 			ExpectedErrMsg: "fetching payment links failed: The api key/secret provided is invalid", // nolint:lll
643 | 		},
644 | 	}
645 | 
646 | 	for _, tc := range tests {
647 | 		t.Run(tc.Name, func(t *testing.T) {
648 | 			toolFunc := FetchAllPaymentLinks
649 | 			runToolTest(t, tc, toolFunc, "Payment Links")
650 | 		})
651 | 	}
652 | }
653 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/payment_links.go:
--------------------------------------------------------------------------------

```go
  1 | package razorpay
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 
  7 | 	rzpsdk "github.com/razorpay/razorpay-go"
  8 | 
  9 | 	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
 10 | 	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
 11 | )
 12 | 
 13 | // CreatePaymentLink returns a tool that creates payment links in Razorpay
 14 | func CreatePaymentLink(
 15 | 	obs *observability.Observability,
 16 | 	client *rzpsdk.Client,
 17 | ) mcpgo.Tool {
 18 | 	parameters := []mcpgo.ToolParameter{
 19 | 		mcpgo.WithNumber(
 20 | 			"amount",
 21 | 			mcpgo.Description("Amount to be paid using the link in smallest "+
 22 | 				"currency unit(e.g., ₹300, use 30000)"),
 23 | 			mcpgo.Required(),
 24 | 			mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
 25 | 		),
 26 | 		mcpgo.WithString(
 27 | 			"currency",
 28 | 			mcpgo.Description("Three-letter ISO code for the currency (e.g., INR)"),
 29 | 			mcpgo.Required(),
 30 | 		),
 31 | 		mcpgo.WithString(
 32 | 			"description",
 33 | 			mcpgo.Description("A brief description of the Payment Link explaining the intent of the payment."), // nolint:lll
 34 | 		),
 35 | 		mcpgo.WithBoolean(
 36 | 			"accept_partial",
 37 | 			mcpgo.Description("Indicates whether customers can make partial payments using the Payment Link. Default: false"), // nolint:lll
 38 | 		),
 39 | 		mcpgo.WithNumber(
 40 | 			"first_min_partial_amount",
 41 | 			mcpgo.Description("Minimum amount that must be paid by the customer as the first partial payment. Default value is 100."), // nolint:lll
 42 | 		),
 43 | 		mcpgo.WithNumber(
 44 | 			"expire_by",
 45 | 			mcpgo.Description("Timestamp, in Unix, when the Payment Link will expire. By default, a Payment Link will be valid for six months."), // nolint:lll
 46 | 		),
 47 | 		mcpgo.WithString(
 48 | 			"reference_id",
 49 | 			mcpgo.Description("Reference number tagged to a Payment Link. Must be unique for each Payment Link. Max 40 characters."), // nolint:lll
 50 | 		),
 51 | 		mcpgo.WithString(
 52 | 			"customer_name",
 53 | 			mcpgo.Description("Name of the customer."),
 54 | 		),
 55 | 		mcpgo.WithString(
 56 | 			"customer_email",
 57 | 			mcpgo.Description("Email address of the customer."),
 58 | 		),
 59 | 		mcpgo.WithString(
 60 | 			"customer_contact",
 61 | 			mcpgo.Description("Contact number of the customer."),
 62 | 		),
 63 | 		mcpgo.WithBoolean(
 64 | 			"notify_sms",
 65 | 			mcpgo.Description("Send SMS notifications for the Payment Link."),
 66 | 		),
 67 | 		mcpgo.WithBoolean(
 68 | 			"notify_email",
 69 | 			mcpgo.Description("Send email notifications for the Payment Link."),
 70 | 		),
 71 | 		mcpgo.WithBoolean(
 72 | 			"reminder_enable",
 73 | 			mcpgo.Description("Enable payment reminders for the Payment Link."),
 74 | 		),
 75 | 		mcpgo.WithObject(
 76 | 			"notes",
 77 | 			mcpgo.Description("Key-value pairs that can be used to store additional information. Maximum 15 pairs, each value limited to 256 characters."), // nolint:lll
 78 | 		),
 79 | 		mcpgo.WithString(
 80 | 			"callback_url",
 81 | 			mcpgo.Description("If specified, adds a redirect URL to the Payment Link. Customer will be redirected here after payment."), // nolint:lll
 82 | 		),
 83 | 		mcpgo.WithString(
 84 | 			"callback_method",
 85 | 			mcpgo.Description("HTTP method for callback redirection. "+
 86 | 				"Must be 'get' if callback_url is set."),
 87 | 		),
 88 | 	}
 89 | 
 90 | 	handler := func(
 91 | 		ctx context.Context,
 92 | 		r mcpgo.CallToolRequest,
 93 | 	) (*mcpgo.ToolResult, error) {
 94 | 		// Get client from context or use default
 95 | 		client, err := getClientFromContextOrDefault(ctx, client)
 96 | 		if err != nil {
 97 | 			return mcpgo.NewToolResultError(err.Error()), nil
 98 | 		}
 99 | 
100 | 		// Create a parameters map to collect validated parameters
101 | 		plCreateReq := make(map[string]interface{})
102 | 		customer := make(map[string]interface{})
103 | 		notify := make(map[string]interface{})
104 | 		// Validate all parameters with fluent validator
105 | 		validator := NewValidator(&r).
106 | 			ValidateAndAddRequiredInt(plCreateReq, "amount").
107 | 			ValidateAndAddRequiredString(plCreateReq, "currency").
108 | 			ValidateAndAddOptionalString(plCreateReq, "description").
109 | 			ValidateAndAddOptionalBool(plCreateReq, "accept_partial").
110 | 			ValidateAndAddOptionalInt(plCreateReq, "first_min_partial_amount").
111 | 			ValidateAndAddOptionalInt(plCreateReq, "expire_by").
112 | 			ValidateAndAddOptionalString(plCreateReq, "reference_id").
113 | 			ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
114 | 			ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
115 | 			ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact").
116 | 			ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
117 | 			ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email").
118 | 			ValidateAndAddOptionalBool(plCreateReq, "reminder_enable").
119 | 			ValidateAndAddOptionalMap(plCreateReq, "notes").
120 | 			ValidateAndAddOptionalString(plCreateReq, "callback_url").
121 | 			ValidateAndAddOptionalString(plCreateReq, "callback_method")
122 | 
123 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
124 | 			return result, err
125 | 		}
126 | 
127 | 		// Handle customer details
128 | 		if len(customer) > 0 {
129 | 			plCreateReq["customer"] = customer
130 | 		}
131 | 
132 | 		// Handle notification settings
133 | 		if len(notify) > 0 {
134 | 			plCreateReq["notify"] = notify
135 | 		}
136 | 
137 | 		// Create the payment link
138 | 		paymentLink, err := client.PaymentLink.Create(plCreateReq, nil)
139 | 		if err != nil {
140 | 			return mcpgo.NewToolResultError(
141 | 				fmt.Sprintf("creating payment link failed: %s", err.Error())), nil
142 | 		}
143 | 
144 | 		return mcpgo.NewToolResultJSON(paymentLink)
145 | 	}
146 | 
147 | 	return mcpgo.NewTool(
148 | 		"create_payment_link",
149 | 		"Create a new standard payment link in Razorpay with a specified amount",
150 | 		parameters,
151 | 		handler,
152 | 	)
153 | }
154 | 
155 | // CreateUpiPaymentLink returns a tool that creates payment links in Razorpay
156 | func CreateUpiPaymentLink(
157 | 	obs *observability.Observability,
158 | 	client *rzpsdk.Client,
159 | ) mcpgo.Tool {
160 | 	parameters := []mcpgo.ToolParameter{
161 | 		mcpgo.WithNumber(
162 | 			"amount",
163 | 			mcpgo.Description("Amount to be paid using the link in smallest currency unit(e.g., ₹300, use 30000), Only accepted currency is INR"), // nolint:lll
164 | 			mcpgo.Required(),
165 | 			mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
166 | 		),
167 | 		mcpgo.WithString(
168 | 			"currency",
169 | 			mcpgo.Description("Three-letter ISO code for the currency (e.g., INR). UPI links are only supported in INR"), // nolint:lll
170 | 			mcpgo.Required(),
171 | 		),
172 | 		mcpgo.WithString(
173 | 			"description",
174 | 			mcpgo.Description("A brief description of the Payment Link explaining the intent of the payment."), // nolint:lll
175 | 		),
176 | 		mcpgo.WithBoolean(
177 | 			"accept_partial",
178 | 			mcpgo.Description("Indicates whether customers can make partial payments using the Payment Link. Default: false"), // nolint:lll
179 | 		),
180 | 		mcpgo.WithNumber(
181 | 			"first_min_partial_amount",
182 | 			mcpgo.Description("Minimum amount that must be paid by the customer as the first partial payment. Default value is 100."), // nolint:lll
183 | 		),
184 | 		mcpgo.WithNumber(
185 | 			"expire_by",
186 | 			mcpgo.Description("Timestamp, in Unix, when the Payment Link will expire. By default, a Payment Link will be valid for six months."), // nolint:lll
187 | 		),
188 | 		mcpgo.WithString(
189 | 			"reference_id",
190 | 			mcpgo.Description("Reference number tagged to a Payment Link. Must be unique for each Payment Link. Max 40 characters."), // nolint:lll
191 | 		),
192 | 		mcpgo.WithString(
193 | 			"customer_name",
194 | 			mcpgo.Description("Name of the customer."),
195 | 		),
196 | 		mcpgo.WithString(
197 | 			"customer_email",
198 | 			mcpgo.Description("Email address of the customer."),
199 | 		),
200 | 		mcpgo.WithString(
201 | 			"customer_contact",
202 | 			mcpgo.Description("Contact number of the customer."),
203 | 		),
204 | 		mcpgo.WithBoolean(
205 | 			"notify_sms",
206 | 			mcpgo.Description("Send SMS notifications for the Payment Link."),
207 | 		),
208 | 		mcpgo.WithBoolean(
209 | 			"notify_email",
210 | 			mcpgo.Description("Send email notifications for the Payment Link."),
211 | 		),
212 | 		mcpgo.WithBoolean(
213 | 			"reminder_enable",
214 | 			mcpgo.Description("Enable payment reminders for the Payment Link."),
215 | 		),
216 | 		mcpgo.WithObject(
217 | 			"notes",
218 | 			mcpgo.Description("Key-value pairs that can be used to store additional information. Maximum 15 pairs, each value limited to 256 characters."), // nolint:lll
219 | 		),
220 | 		mcpgo.WithString(
221 | 			"callback_url",
222 | 			mcpgo.Description("If specified, adds a redirect URL to the Payment Link. Customer will be redirected here after payment."), // nolint:lll
223 | 		),
224 | 		mcpgo.WithString(
225 | 			"callback_method",
226 | 			mcpgo.Description("HTTP method for callback redirection. "+
227 | 				"Must be 'get' if callback_url is set."),
228 | 		),
229 | 	}
230 | 
231 | 	handler := func(
232 | 		ctx context.Context,
233 | 		r mcpgo.CallToolRequest,
234 | 	) (*mcpgo.ToolResult, error) {
235 | 		// Create a parameters map to collect validated parameters
236 | 		upiPlCreateReq := make(map[string]interface{})
237 | 		customer := make(map[string]interface{})
238 | 		notify := make(map[string]interface{})
239 | 		// Validate all parameters with fluent validator
240 | 		validator := NewValidator(&r).
241 | 			ValidateAndAddRequiredInt(upiPlCreateReq, "amount").
242 | 			ValidateAndAddRequiredString(upiPlCreateReq, "currency").
243 | 			ValidateAndAddOptionalString(upiPlCreateReq, "description").
244 | 			ValidateAndAddOptionalBool(upiPlCreateReq, "accept_partial").
245 | 			ValidateAndAddOptionalInt(upiPlCreateReq, "first_min_partial_amount").
246 | 			ValidateAndAddOptionalInt(upiPlCreateReq, "expire_by").
247 | 			ValidateAndAddOptionalString(upiPlCreateReq, "reference_id").
248 | 			ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
249 | 			ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
250 | 			ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact").
251 | 			ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
252 | 			ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email").
253 | 			ValidateAndAddOptionalBool(upiPlCreateReq, "reminder_enable").
254 | 			ValidateAndAddOptionalMap(upiPlCreateReq, "notes").
255 | 			ValidateAndAddOptionalString(upiPlCreateReq, "callback_url").
256 | 			ValidateAndAddOptionalString(upiPlCreateReq, "callback_method")
257 | 
258 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
259 | 			return result, err
260 | 		}
261 | 
262 | 		// Add the required UPI payment link parameters
263 | 		upiPlCreateReq["upi_link"] = "true"
264 | 
265 | 		// Handle customer details
266 | 		if len(customer) > 0 {
267 | 			upiPlCreateReq["customer"] = customer
268 | 		}
269 | 
270 | 		// Handle notification settings
271 | 		if len(notify) > 0 {
272 | 			upiPlCreateReq["notify"] = notify
273 | 		}
274 | 
275 | 		client, err := getClientFromContextOrDefault(ctx, client)
276 | 		if err != nil {
277 | 			return mcpgo.NewToolResultError(err.Error()), nil
278 | 		}
279 | 
280 | 		// Create the payment link
281 | 		paymentLink, err := client.PaymentLink.Create(upiPlCreateReq, nil)
282 | 		if err != nil {
283 | 			return mcpgo.NewToolResultError(
284 | 				fmt.Sprintf("upi pl create failed: %s", err.Error())), nil
285 | 		}
286 | 
287 | 		return mcpgo.NewToolResultJSON(paymentLink)
288 | 	}
289 | 
290 | 	return mcpgo.NewTool(
291 | 		"payment_link_upi_create",
292 | 		"Create a new UPI payment link in Razorpay with a specified amount and additional options.", // nolint:lll
293 | 		parameters,
294 | 		handler,
295 | 	)
296 | }
297 | 
298 | // FetchPaymentLink returns a tool that fetches payment link details using
299 | // payment_link_id
300 | func FetchPaymentLink(
301 | 	obs *observability.Observability,
302 | 	client *rzpsdk.Client,
303 | ) mcpgo.Tool {
304 | 	parameters := []mcpgo.ToolParameter{
305 | 		mcpgo.WithString(
306 | 			"payment_link_id",
307 | 			mcpgo.Description("ID of the payment link to be fetched"+
308 | 				"(ID should have a plink_ prefix)."),
309 | 			mcpgo.Required(),
310 | 		),
311 | 	}
312 | 
313 | 	handler := func(
314 | 		ctx context.Context,
315 | 		r mcpgo.CallToolRequest,
316 | 	) (*mcpgo.ToolResult, error) {
317 | 		// Get client from context or use default
318 | 		client, err := getClientFromContextOrDefault(ctx, client)
319 | 		if err != nil {
320 | 			return mcpgo.NewToolResultError(err.Error()), nil
321 | 		}
322 | 
323 | 		fields := make(map[string]interface{})
324 | 
325 | 		validator := NewValidator(&r).
326 | 			ValidateAndAddRequiredString(fields, "payment_link_id")
327 | 
328 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
329 | 			return result, err
330 | 		}
331 | 
332 | 		paymentLinkId := fields["payment_link_id"].(string)
333 | 
334 | 		paymentLink, err := client.PaymentLink.Fetch(paymentLinkId, nil, nil)
335 | 		if err != nil {
336 | 			return mcpgo.NewToolResultError(
337 | 				fmt.Sprintf("fetching payment link failed: %s", err.Error())), nil
338 | 		}
339 | 
340 | 		return mcpgo.NewToolResultJSON(paymentLink)
341 | 	}
342 | 
343 | 	return mcpgo.NewTool(
344 | 		"fetch_payment_link",
345 | 		"Fetch payment link details using it's ID. "+
346 | 			"Response contains the basic details like amount, status etc. "+
347 | 			"The link could be of any type(standard or UPI)",
348 | 		parameters,
349 | 		handler,
350 | 	)
351 | }
352 | 
353 | // ResendPaymentLinkNotification returns a tool that sends/resends notifications
354 | // for a payment link via email or SMS
355 | func ResendPaymentLinkNotification(
356 | 	obs *observability.Observability,
357 | 	client *rzpsdk.Client,
358 | ) mcpgo.Tool {
359 | 	parameters := []mcpgo.ToolParameter{
360 | 		mcpgo.WithString(
361 | 			"payment_link_id",
362 | 			mcpgo.Description("ID of the payment link for which to send notification "+
363 | 				"(ID should have a plink_ prefix)."), // nolint:lll
364 | 			mcpgo.Required(),
365 | 		),
366 | 		mcpgo.WithString(
367 | 			"medium",
368 | 			mcpgo.Description("Medium through which to send the notification. "+
369 | 				"Must be either 'sms' or 'email'."), // nolint:lll
370 | 			mcpgo.Required(),
371 | 			mcpgo.Enum("sms", "email"),
372 | 		),
373 | 	}
374 | 
375 | 	handler := func(
376 | 		ctx context.Context,
377 | 		r mcpgo.CallToolRequest,
378 | 	) (*mcpgo.ToolResult, error) {
379 | 		client, err := getClientFromContextOrDefault(ctx, client)
380 | 		if err != nil {
381 | 			return mcpgo.NewToolResultError(err.Error()), nil
382 | 		}
383 | 
384 | 		fields := make(map[string]interface{})
385 | 
386 | 		validator := NewValidator(&r).
387 | 			ValidateAndAddRequiredString(fields, "payment_link_id").
388 | 			ValidateAndAddRequiredString(fields, "medium")
389 | 
390 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
391 | 			return result, err
392 | 		}
393 | 
394 | 		paymentLinkId := fields["payment_link_id"].(string)
395 | 		medium := fields["medium"].(string)
396 | 
397 | 		// Call the SDK function
398 | 		response, err := client.PaymentLink.NotifyBy(paymentLinkId, medium, nil, nil)
399 | 		if err != nil {
400 | 			return mcpgo.NewToolResultError(
401 | 				fmt.Sprintf("sending notification failed: %s", err.Error())), nil
402 | 		}
403 | 
404 | 		return mcpgo.NewToolResultJSON(response)
405 | 	}
406 | 
407 | 	return mcpgo.NewTool(
408 | 		"payment_link_notify",
409 | 		"Send or resend notification for a payment link via SMS or email.", // nolint:lll
410 | 		parameters,
411 | 		handler,
412 | 	)
413 | }
414 | 
415 | // UpdatePaymentLink returns a tool that updates an existing payment link
416 | func UpdatePaymentLink(
417 | 	obs *observability.Observability,
418 | 	client *rzpsdk.Client,
419 | ) mcpgo.Tool {
420 | 	parameters := []mcpgo.ToolParameter{
421 | 		mcpgo.WithString(
422 | 			"payment_link_id",
423 | 			mcpgo.Description("ID of the payment link to update "+
424 | 				"(ID should have a plink_ prefix)."),
425 | 			mcpgo.Required(),
426 | 		),
427 | 		mcpgo.WithString(
428 | 			"reference_id",
429 | 			mcpgo.Description("Adds a unique reference number to the payment link."),
430 | 		),
431 | 		mcpgo.WithNumber(
432 | 			"expire_by",
433 | 			mcpgo.Description("Timestamp, in Unix format, when the payment link "+
434 | 				"should expire."),
435 | 		),
436 | 		mcpgo.WithBoolean(
437 | 			"reminder_enable",
438 | 			mcpgo.Description("Enable or disable reminders for the payment link."),
439 | 		),
440 | 		mcpgo.WithBoolean(
441 | 			"accept_partial",
442 | 			mcpgo.Description("Allow customers to make partial payments. "+
443 | 				"Not allowed with UPI payment links."),
444 | 		),
445 | 		mcpgo.WithObject(
446 | 			"notes",
447 | 			mcpgo.Description("Key-value pairs for additional information. "+
448 | 				"Maximum 15 pairs, each value limited to 256 characters."),
449 | 		),
450 | 	}
451 | 
452 | 	handler := func(
453 | 		ctx context.Context,
454 | 		r mcpgo.CallToolRequest,
455 | 	) (*mcpgo.ToolResult, error) {
456 | 		client, err := getClientFromContextOrDefault(ctx, client)
457 | 		if err != nil {
458 | 			return mcpgo.NewToolResultError(err.Error()), nil
459 | 		}
460 | 
461 | 		plUpdateReq := make(map[string]interface{})
462 | 		otherFields := make(map[string]interface{})
463 | 
464 | 		validator := NewValidator(&r).
465 | 			ValidateAndAddRequiredString(otherFields, "payment_link_id").
466 | 			ValidateAndAddOptionalString(plUpdateReq, "reference_id").
467 | 			ValidateAndAddOptionalInt(plUpdateReq, "expire_by").
468 | 			ValidateAndAddOptionalBool(plUpdateReq, "reminder_enable").
469 | 			ValidateAndAddOptionalBool(plUpdateReq, "accept_partial").
470 | 			ValidateAndAddOptionalMap(plUpdateReq, "notes")
471 | 
472 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
473 | 			return result, err
474 | 		}
475 | 
476 | 		paymentLinkId := otherFields["payment_link_id"].(string)
477 | 
478 | 		// Ensure we have at least one field to update
479 | 		if len(plUpdateReq) == 0 {
480 | 			return mcpgo.NewToolResultError(
481 | 				"at least one field to update must be provided"), nil
482 | 		}
483 | 
484 | 		// Call the SDK function
485 | 		paymentLink, err := client.PaymentLink.Update(paymentLinkId, plUpdateReq, nil)
486 | 		if err != nil {
487 | 			return mcpgo.NewToolResultError(
488 | 				fmt.Sprintf("updating payment link failed: %s", err.Error())), nil
489 | 		}
490 | 
491 | 		return mcpgo.NewToolResultJSON(paymentLink)
492 | 	}
493 | 
494 | 	return mcpgo.NewTool(
495 | 		"update_payment_link",
496 | 		"Update any existing standard or UPI payment link with new details such as reference ID, "+ // nolint:lll
497 | 			"expiry date, or notes.",
498 | 		parameters,
499 | 		handler,
500 | 	)
501 | }
502 | 
503 | // FetchAllPaymentLinks returns a tool that fetches all payment links
504 | // with optional filtering
505 | func FetchAllPaymentLinks(
506 | 	obs *observability.Observability,
507 | 	client *rzpsdk.Client,
508 | ) mcpgo.Tool {
509 | 	parameters := []mcpgo.ToolParameter{
510 | 		mcpgo.WithString(
511 | 			"payment_id",
512 | 			mcpgo.Description("Optional: Filter by payment ID associated with payment links"), // nolint:lll
513 | 		),
514 | 		mcpgo.WithString(
515 | 			"reference_id",
516 | 			mcpgo.Description("Optional: Filter by reference ID used when creating payment links"), // nolint:lll
517 | 		),
518 | 		mcpgo.WithNumber(
519 | 			"upi_link",
520 | 			mcpgo.Description("Optional: Filter only upi links. "+
521 | 				"Value should be 1 if you want only upi links, 0 for only standard links"+
522 | 				"If not provided, all types of links will be returned"),
523 | 		),
524 | 	}
525 | 
526 | 	handler := func(
527 | 		ctx context.Context,
528 | 		r mcpgo.CallToolRequest,
529 | 	) (*mcpgo.ToolResult, error) {
530 | 		client, err := getClientFromContextOrDefault(ctx, client)
531 | 		if err != nil {
532 | 			return mcpgo.NewToolResultError(err.Error()), nil
533 | 		}
534 | 
535 | 		plListReq := make(map[string]interface{})
536 | 
537 | 		validator := NewValidator(&r).
538 | 			ValidateAndAddOptionalString(plListReq, "payment_id").
539 | 			ValidateAndAddOptionalString(plListReq, "reference_id").
540 | 			ValidateAndAddOptionalInt(plListReq, "upi_link")
541 | 
542 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
543 | 			return result, err
544 | 		}
545 | 
546 | 		// Call the API directly using the Request object
547 | 		response, err := client.PaymentLink.All(plListReq, nil)
548 | 		if err != nil {
549 | 			return mcpgo.NewToolResultError(
550 | 				fmt.Sprintf("fetching payment links failed: %s", err.Error())), nil
551 | 		}
552 | 
553 | 		return mcpgo.NewToolResultJSON(response)
554 | 	}
555 | 
556 | 	return mcpgo.NewTool(
557 | 		"fetch_all_payment_links",
558 | 		"Fetch all payment links with optional filtering by payment ID or reference ID."+ // nolint:lll
559 | 			"You can specify the upi_link parameter to filter by link type.",
560 | 		parameters,
561 | 		handler,
562 | 	)
563 | }
564 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/refunds_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_CreateRefund(t *testing.T) {
 15 | 	createRefundPathFmt := fmt.Sprintf(
 16 | 		"/%s%s/%%s/refund",
 17 | 		constants.VERSION_V1,
 18 | 		constants.PAYMENT_URL,
 19 | 	)
 20 | 
 21 | 	// Define test responses
 22 | 	successfulRefundResp := map[string]interface{}{
 23 | 		"id":              "rfnd_FP8QHiV938haTz",
 24 | 		"entity":          "refund",
 25 | 		"amount":          float64(500100),
 26 | 		"currency":        "INR",
 27 | 		"payment_id":      "pay_29QQoUBi66xm2f",
 28 | 		"notes":           map[string]interface{}{},
 29 | 		"receipt":         "Receipt No. 31",
 30 | 		"acquirer_data":   map[string]interface{}{"arn": nil},
 31 | 		"created_at":      float64(1597078866),
 32 | 		"batch_id":        nil,
 33 | 		"status":          "processed",
 34 | 		"speed_processed": "normal",
 35 | 		"speed_requested": "normal",
 36 | 	}
 37 | 
 38 | 	errorResp := map[string]interface{}{
 39 | 		"error": map[string]interface{}{
 40 | 			"code":        "BAD_REQUEST_ERROR",
 41 | 			"description": "Razorpay API error: Bad request",
 42 | 		},
 43 | 	}
 44 | 
 45 | 	tests := []RazorpayToolTestCase{
 46 | 		{
 47 | 			Name: "successful full refund",
 48 | 			Request: map[string]interface{}{
 49 | 				"payment_id": "pay_29QQoUBi66xm2f",
 50 | 				"amount":     float64(500100),
 51 | 				"receipt":    "Receipt No. 31",
 52 | 			},
 53 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 54 | 				return mock.NewHTTPClient(
 55 | 					mock.Endpoint{
 56 | 						Path:     fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
 57 | 						Method:   "POST",
 58 | 						Response: successfulRefundResp,
 59 | 					},
 60 | 				)
 61 | 			},
 62 | 			ExpectError:    false,
 63 | 			ExpectedResult: successfulRefundResp,
 64 | 		},
 65 | 		{
 66 | 			Name: "refund with speed parameter",
 67 | 			Request: map[string]interface{}{
 68 | 				"payment_id": "pay_29QQoUBi66xm2f",
 69 | 				"amount":     float64(500100),
 70 | 				"speed":      "optimum",
 71 | 			},
 72 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 73 | 				speedRefundResp := map[string]interface{}{
 74 | 					"id":              "rfnd_HzAbPEkKtRq48V",
 75 | 					"entity":          "refund",
 76 | 					"amount":          float64(500100),
 77 | 					"payment_id":      "pay_29QQoUBi66xm2f",
 78 | 					"status":          "processed",
 79 | 					"speed_processed": "instant",
 80 | 					"speed_requested": "optimum",
 81 | 				}
 82 | 				return mock.NewHTTPClient(
 83 | 					mock.Endpoint{
 84 | 						Path:     fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
 85 | 						Method:   "POST",
 86 | 						Response: speedRefundResp,
 87 | 					},
 88 | 				)
 89 | 			},
 90 | 			ExpectError: false,
 91 | 			ExpectedResult: map[string]interface{}{
 92 | 				"id":              "rfnd_HzAbPEkKtRq48V",
 93 | 				"entity":          "refund",
 94 | 				"amount":          float64(500100),
 95 | 				"payment_id":      "pay_29QQoUBi66xm2f",
 96 | 				"status":          "processed",
 97 | 				"speed_processed": "instant",
 98 | 				"speed_requested": "optimum",
 99 | 			},
100 | 		},
101 | 		{
102 | 			Name: "refund API server error",
103 | 			Request: map[string]interface{}{
104 | 				"payment_id": "pay_29QQoUBi66xm2f",
105 | 				"amount":     float64(500100),
106 | 			},
107 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
108 | 				return mock.NewHTTPClient(
109 | 					mock.Endpoint{
110 | 						Path:     fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
111 | 						Method:   "POST",
112 | 						Response: errorResp,
113 | 					},
114 | 				)
115 | 			},
116 | 			ExpectError:    true,
117 | 			ExpectedErrMsg: "creating refund failed: Razorpay API error: Bad request",
118 | 		},
119 | 		{
120 | 			Name: "multiple validation errors",
121 | 			Request: map[string]interface{}{
122 | 				// Missing payment_id parameter
123 | 				"amount": "not-a-number",  // Wrong type for amount
124 | 				"speed":  12345,           // Wrong type for speed
125 | 				"notes":  "not-an-object", // Wrong type for notes
126 | 			},
127 | 			MockHttpClient: nil,
128 | 			ExpectError:    true,
129 | 			ExpectedErrMsg: "Validation errors:\n- " +
130 | 				"missing required parameter: payment_id\n- " +
131 | 				"invalid parameter type: amount\n- " +
132 | 				"invalid parameter type: speed\n- " +
133 | 				"invalid parameter type: notes",
134 | 		},
135 | 	}
136 | 
137 | 	for _, tc := range tests {
138 | 		t.Run(tc.Name, func(t *testing.T) {
139 | 			runToolTest(t, tc, CreateRefund, "Refund")
140 | 		})
141 | 	}
142 | }
143 | 
144 | func Test_FetchRefund(t *testing.T) {
145 | 	fetchRefundPathFmt := fmt.Sprintf(
146 | 		"/%s%s/%%s",
147 | 		constants.VERSION_V1,
148 | 		constants.REFUND_URL,
149 | 	)
150 | 
151 | 	// Define test response for successful refund fetch
152 | 	successfulRefundResp := map[string]interface{}{
153 | 		"id":         "rfnd_DfjjhJC6eDvUAi",
154 | 		"entity":     "refund",
155 | 		"amount":     float64(6000),
156 | 		"currency":   "INR",
157 | 		"payment_id": "pay_EpkFDYRirena0f",
158 | 		"notes": map[string]interface{}{
159 | 			"comment": "Issuing an instant refund",
160 | 		},
161 | 		"receipt": nil,
162 | 		"acquirer_data": map[string]interface{}{
163 | 			"arn": "10000000000000",
164 | 		},
165 | 		"created_at":      float64(1589521675),
166 | 		"batch_id":        nil,
167 | 		"status":          "processed",
168 | 		"speed_processed": "optimum",
169 | 		"speed_requested": "optimum",
170 | 	}
171 | 
172 | 	// Define error responses
173 | 	notFoundResp := map[string]interface{}{
174 | 		"error": map[string]interface{}{
175 | 			"code":        "BAD_REQUEST_ERROR",
176 | 			"description": "The id provided does not exist",
177 | 		},
178 | 	}
179 | 
180 | 	tests := []RazorpayToolTestCase{
181 | 		{
182 | 			Name: "successful refund fetch",
183 | 			Request: map[string]interface{}{
184 | 				"refund_id": "rfnd_DfjjhJC6eDvUAi",
185 | 			},
186 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
187 | 				return mock.NewHTTPClient(
188 | 					mock.Endpoint{
189 | 						Path:     fmt.Sprintf(fetchRefundPathFmt, "rfnd_DfjjhJC6eDvUAi"),
190 | 						Method:   "GET",
191 | 						Response: successfulRefundResp,
192 | 					},
193 | 				)
194 | 			},
195 | 			ExpectError:    false,
196 | 			ExpectedResult: successfulRefundResp,
197 | 		},
198 | 		{
199 | 			Name: "refund id not found",
200 | 			Request: map[string]interface{}{
201 | 				"refund_id": "rfnd_nonexistent",
202 | 			},
203 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
204 | 				return mock.NewHTTPClient(
205 | 					mock.Endpoint{
206 | 						Path:     fmt.Sprintf(fetchRefundPathFmt, "rfnd_nonexistent"),
207 | 						Method:   "GET",
208 | 						Response: notFoundResp,
209 | 					},
210 | 				)
211 | 			},
212 | 			ExpectError:    true,
213 | 			ExpectedErrMsg: "fetching refund failed: The id provided does not exist",
214 | 		},
215 | 		{
216 | 			Name:           "missing refund_id parameter",
217 | 			Request:        map[string]interface{}{},
218 | 			MockHttpClient: nil,
219 | 			ExpectError:    true,
220 | 			ExpectedErrMsg: "missing required parameter: refund_id",
221 | 		},
222 | 		{
223 | 			Name: "multiple validation errors",
224 | 			Request: map[string]interface{}{
225 | 				// Missing refund_id parameter
226 | 				"non_existent_param": 12345, // Additional parameter that doesn't exist
227 | 			},
228 | 			MockHttpClient: nil,
229 | 			ExpectError:    true,
230 | 			ExpectedErrMsg: "missing required parameter: refund_id",
231 | 		},
232 | 	}
233 | 
234 | 	for _, tc := range tests {
235 | 		t.Run(tc.Name, func(t *testing.T) {
236 | 			runToolTest(t, tc, FetchRefund, "Refund")
237 | 		})
238 | 	}
239 | }
240 | 
241 | func Test_UpdateRefund(t *testing.T) {
242 | 	updateRefundPathFmt := fmt.Sprintf(
243 | 		"/%s%s/%%s",
244 | 		constants.VERSION_V1,
245 | 		constants.REFUND_URL,
246 | 	)
247 | 
248 | 	// Define test response for successful refund update
249 | 	successfulUpdateResp := map[string]interface{}{
250 | 		"id":         "rfnd_DfjjhJC6eDvUAi",
251 | 		"entity":     "refund",
252 | 		"amount":     float64(300100),
253 | 		"currency":   "INR",
254 | 		"payment_id": "pay_FIKOnlyii5QGNx",
255 | 		"notes": map[string]interface{}{
256 | 			"notes_key_1": "Beam me up Scotty.",
257 | 			"notes_key_2": "Engage",
258 | 		},
259 | 		"receipt":         nil,
260 | 		"acquirer_data":   map[string]interface{}{"arn": "10000000000000"},
261 | 		"created_at":      float64(1597078124),
262 | 		"batch_id":        nil,
263 | 		"status":          "processed",
264 | 		"speed_processed": "normal",
265 | 		"speed_requested": "optimum",
266 | 	}
267 | 
268 | 	// Define error responses
269 | 	notFoundResp := map[string]interface{}{
270 | 		"error": map[string]interface{}{
271 | 			"code":        "BAD_REQUEST_ERROR",
272 | 			"description": "The id provided does not exist",
273 | 		},
274 | 	}
275 | 
276 | 	tests := []RazorpayToolTestCase{
277 | 		{
278 | 			Name: "successful refund update",
279 | 			Request: map[string]interface{}{
280 | 				"refund_id": "rfnd_DfjjhJC6eDvUAi",
281 | 				"notes": map[string]interface{}{
282 | 					"notes_key_1": "Beam me up Scotty.",
283 | 					"notes_key_2": "Engage",
284 | 				},
285 | 			},
286 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
287 | 				return mock.NewHTTPClient(
288 | 					mock.Endpoint{
289 | 						Path:     fmt.Sprintf(updateRefundPathFmt, "rfnd_DfjjhJC6eDvUAi"),
290 | 						Method:   "PATCH",
291 | 						Response: successfulUpdateResp,
292 | 					},
293 | 				)
294 | 			},
295 | 			ExpectError:    false,
296 | 			ExpectedResult: successfulUpdateResp,
297 | 		},
298 | 		{
299 | 			Name: "refund id not found",
300 | 			Request: map[string]interface{}{
301 | 				"refund_id": "rfnd_nonexistent",
302 | 				"notes": map[string]interface{}{
303 | 					"note_key": "Test note",
304 | 				},
305 | 			},
306 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
307 | 				return mock.NewHTTPClient(
308 | 					mock.Endpoint{
309 | 						Path:     fmt.Sprintf(updateRefundPathFmt, "rfnd_nonexistent"),
310 | 						Method:   "PATCH",
311 | 						Response: notFoundResp,
312 | 					},
313 | 				)
314 | 			},
315 | 			ExpectError:    true,
316 | 			ExpectedErrMsg: "updating refund failed: The id provided does not exist",
317 | 		},
318 | 		{
319 | 			Name:           "missing refund_id parameter",
320 | 			Request:        map[string]interface{}{},
321 | 			MockHttpClient: nil,
322 | 			ExpectError:    true,
323 | 			ExpectedErrMsg: "missing required parameter: refund_id",
324 | 		},
325 | 		{
326 | 			Name: "missing notes parameter",
327 | 			Request: map[string]interface{}{
328 | 				"refund_id": "rfnd_DfjjhJC6eDvUAi",
329 | 			},
330 | 			MockHttpClient: nil,
331 | 			ExpectError:    true,
332 | 			ExpectedErrMsg: "missing required parameter: notes",
333 | 		},
334 | 		{
335 | 			Name: "multiple validation errors",
336 | 			Request: map[string]interface{}{
337 | 				// Missing both refund_id and notes parameters
338 | 				"non_existent_param": 12345, // Additional parameter that doesn't exist
339 | 			},
340 | 			MockHttpClient: nil,
341 | 			ExpectError:    true,
342 | 			ExpectedErrMsg: "Validation errors:\n- " +
343 | 				"missing required parameter: refund_id\n- " +
344 | 				"missing required parameter: notes",
345 | 		},
346 | 	}
347 | 
348 | 	for _, tc := range tests {
349 | 		t.Run(tc.Name, func(t *testing.T) {
350 | 			runToolTest(t, tc, UpdateRefund, "Refund")
351 | 		})
352 | 	}
353 | }
354 | 
355 | func Test_FetchMultipleRefundsForPayment(t *testing.T) {
356 | 	fetchMultipleRefundsPathFmt := fmt.Sprintf(
357 | 		"/%s%s/%%s/refunds",
358 | 		constants.VERSION_V1,
359 | 		constants.PAYMENT_URL,
360 | 	)
361 | 
362 | 	// Define test response for successful multiple refunds fetch
363 | 	successfulMultipleRefundsResp := map[string]interface{}{
364 | 		"entity": "collection",
365 | 		"count":  float64(2),
366 | 		"items": []interface{}{
367 | 			map[string]interface{}{
368 | 				"id":         "rfnd_FP8DDKxqJif6ca",
369 | 				"entity":     "refund",
370 | 				"amount":     float64(300100),
371 | 				"currency":   "INR",
372 | 				"payment_id": "pay_29QQoUBi66xm2f",
373 | 				"notes": map[string]interface{}{
374 | 					"comment": "Comment for refund",
375 | 				},
376 | 				"receipt": nil,
377 | 				"acquirer_data": map[string]interface{}{
378 | 					"arn": "10000000000000",
379 | 				},
380 | 				"created_at":      float64(1597078124),
381 | 				"batch_id":        nil,
382 | 				"status":          "processed",
383 | 				"speed_processed": "normal",
384 | 				"speed_requested": "optimum",
385 | 			},
386 | 			map[string]interface{}{
387 | 				"id":         "rfnd_FP8DRfu3ygfOaC",
388 | 				"entity":     "refund",
389 | 				"amount":     float64(200000),
390 | 				"currency":   "INR",
391 | 				"payment_id": "pay_29QQoUBi66xm2f",
392 | 				"notes": map[string]interface{}{
393 | 					"comment": "Comment for refund",
394 | 				},
395 | 				"receipt": nil,
396 | 				"acquirer_data": map[string]interface{}{
397 | 					"arn": "10000000000000",
398 | 				},
399 | 				"created_at":      float64(1597078137),
400 | 				"batch_id":        nil,
401 | 				"status":          "processed",
402 | 				"speed_processed": "normal",
403 | 				"speed_requested": "optimum",
404 | 			},
405 | 		},
406 | 	}
407 | 
408 | 	// Define error responses
409 | 	errorResp := map[string]interface{}{
410 | 		"error": map[string]interface{}{
411 | 			"code":        "BAD_REQUEST_ERROR",
412 | 			"description": "Bad request",
413 | 		},
414 | 	}
415 | 
416 | 	tests := []RazorpayToolTestCase{
417 | 		{
418 | 			Name: "fetch multiple refunds with query params",
419 | 			Request: map[string]interface{}{
420 | 				"payment_id": "pay_29QQoUBi66xm2f",
421 | 				"from":       1500826740,
422 | 				"to":         1500826760,
423 | 				"count":      10,
424 | 				"skip":       0,
425 | 			},
426 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
427 | 				return mock.NewHTTPClient(
428 | 					mock.Endpoint{
429 | 						Path: fmt.Sprintf(
430 | 							fetchMultipleRefundsPathFmt,
431 | 							"pay_29QQoUBi66xm2f",
432 | 						),
433 | 						Method:   "GET",
434 | 						Response: successfulMultipleRefundsResp,
435 | 					},
436 | 				)
437 | 			},
438 | 			ExpectError:    false,
439 | 			ExpectedResult: successfulMultipleRefundsResp,
440 | 		},
441 | 		{
442 | 			Name: "fetch multiple refunds api error",
443 | 			Request: map[string]interface{}{
444 | 				"payment_id": "pay_invalid",
445 | 			},
446 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
447 | 				return mock.NewHTTPClient(
448 | 					mock.Endpoint{
449 | 						Path: fmt.Sprintf(
450 | 							fetchMultipleRefundsPathFmt,
451 | 							"pay_invalid",
452 | 						),
453 | 						Method:   "GET",
454 | 						Response: errorResp,
455 | 					},
456 | 				)
457 | 			},
458 | 			ExpectError:    true,
459 | 			ExpectedErrMsg: "fetching multiple refunds failed: Bad request",
460 | 		},
461 | 		{
462 | 			Name:           "missing payment_id parameter",
463 | 			Request:        map[string]interface{}{},
464 | 			MockHttpClient: nil,
465 | 			ExpectError:    true,
466 | 			ExpectedErrMsg: "missing required parameter: payment_id",
467 | 		},
468 | 		{
469 | 			Name: "multiple validation errors",
470 | 			Request: map[string]interface{}{
471 | 				// Missing payment_id parameter
472 | 				"from":  "not-a-number", // Wrong type for from
473 | 				"to":    "not-a-number", // Wrong type for to
474 | 				"count": "not-a-number", // Wrong type for count
475 | 				"skip":  "not-a-number", // Wrong type for skip
476 | 			},
477 | 			MockHttpClient: nil,
478 | 			ExpectError:    true,
479 | 			ExpectedErrMsg: "Validation errors:\n- " +
480 | 				"missing required parameter: payment_id\n- " +
481 | 				"invalid parameter type: from\n- " +
482 | 				"invalid parameter type: to\n- " +
483 | 				"invalid parameter type: count\n- " +
484 | 				"invalid parameter type: skip",
485 | 		},
486 | 	}
487 | 
488 | 	for _, tc := range tests {
489 | 		t.Run(tc.Name, func(t *testing.T) {
490 | 			runToolTest(t, tc, FetchMultipleRefundsForPayment, "Refund")
491 | 		})
492 | 	}
493 | }
494 | 
495 | func Test_FetchSpecificRefundForPayment(t *testing.T) {
496 | 	fetchSpecificRefundPathFmt := fmt.Sprintf(
497 | 		"/%s%s/%%s/refunds/%%s",
498 | 		constants.VERSION_V1,
499 | 		constants.PAYMENT_URL,
500 | 	)
501 | 
502 | 	// Define test response for successful specific refund fetch
503 | 	successfulSpecificRefundResp := map[string]interface{}{
504 | 		"id":         "rfnd_AABBdHIieexn5c",
505 | 		"entity":     "refund",
506 | 		"amount":     float64(300100),
507 | 		"currency":   "INR",
508 | 		"payment_id": "pay_FIKOnlyii5QGNx",
509 | 		"notes": map[string]interface{}{
510 | 			"comment": "Comment for refund",
511 | 		},
512 | 		"receipt":         nil,
513 | 		"acquirer_data":   map[string]interface{}{"arn": "10000000000000"},
514 | 		"created_at":      float64(1597078124),
515 | 		"batch_id":        nil,
516 | 		"status":          "processed",
517 | 		"speed_processed": "normal",
518 | 		"speed_requested": "optimum",
519 | 	}
520 | 
521 | 	// Define error responses
522 | 	notFoundResp := map[string]interface{}{
523 | 		"error": map[string]interface{}{
524 | 			"code":        "BAD_REQUEST_ERROR",
525 | 			"description": "The id provided does not exist",
526 | 		},
527 | 	}
528 | 
529 | 	tests := []RazorpayToolTestCase{
530 | 		{
531 | 			Name: "successful specific refund fetch",
532 | 			Request: map[string]interface{}{
533 | 				"payment_id": "pay_FIKOnlyii5QGNx",
534 | 				"refund_id":  "rfnd_AABBdHIieexn5c",
535 | 			},
536 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
537 | 				return mock.NewHTTPClient(
538 | 					mock.Endpoint{
539 | 						Path: fmt.Sprintf(
540 | 							fetchSpecificRefundPathFmt,
541 | 							"pay_FIKOnlyii5QGNx",
542 | 							"rfnd_AABBdHIieexn5c",
543 | 						),
544 | 						Method:   "GET",
545 | 						Response: successfulSpecificRefundResp,
546 | 					},
547 | 				)
548 | 			},
549 | 			ExpectError:    false,
550 | 			ExpectedResult: successfulSpecificRefundResp,
551 | 		},
552 | 		{
553 | 			Name: "refund id not found",
554 | 			Request: map[string]interface{}{
555 | 				"payment_id": "pay_FIKOnlyii5QGNx",
556 | 				"refund_id":  "rfnd_nonexistent",
557 | 			},
558 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
559 | 				return mock.NewHTTPClient(
560 | 					mock.Endpoint{
561 | 						Path: fmt.Sprintf(
562 | 							fetchSpecificRefundPathFmt,
563 | 							"pay_FIKOnlyii5QGNx",
564 | 							"rfnd_nonexistent",
565 | 						),
566 | 						Method:   "GET",
567 | 						Response: notFoundResp,
568 | 					},
569 | 				)
570 | 			},
571 | 			ExpectError: true,
572 | 			ExpectedErrMsg: "fetching specific refund for payment failed: " +
573 | 				"The id provided does not exist",
574 | 		},
575 | 		{
576 | 			Name: "missing payment_id parameter",
577 | 			Request: map[string]interface{}{
578 | 				"refund_id": "rfnd_AABBdHIieexn5c",
579 | 			},
580 | 			MockHttpClient: nil,
581 | 			ExpectError:    true,
582 | 			ExpectedErrMsg: "missing required parameter: payment_id",
583 | 		},
584 | 		{
585 | 			Name: "missing refund_id parameter",
586 | 			Request: map[string]interface{}{
587 | 				"payment_id": "pay_FIKOnlyii5QGNx",
588 | 			},
589 | 			MockHttpClient: nil,
590 | 			ExpectError:    true,
591 | 			ExpectedErrMsg: "missing required parameter: refund_id",
592 | 		},
593 | 		{
594 | 			Name: "multiple validation errors",
595 | 			Request: map[string]interface{}{
596 | 				// Missing both payment_id and refund_id parameters
597 | 				"non_existent_param": 12345, // Additional parameter that doesn't exist
598 | 			},
599 | 			MockHttpClient: nil,
600 | 			ExpectError:    true,
601 | 			ExpectedErrMsg: "Validation errors:\n- " +
602 | 				"missing required parameter: payment_id\n- " +
603 | 				"missing required parameter: refund_id",
604 | 		},
605 | 	}
606 | 
607 | 	for _, tc := range tests {
608 | 		t.Run(tc.Name, func(t *testing.T) {
609 | 			runToolTest(t, tc, FetchSpecificRefundForPayment, "Refund")
610 | 		})
611 | 	}
612 | }
613 | 
614 | func Test_FetchAllRefunds(t *testing.T) {
615 | 	fetchAllRefundsPath := fmt.Sprintf(
616 | 		"/%s%s",
617 | 		constants.VERSION_V1,
618 | 		constants.REFUND_URL,
619 | 	)
620 | 
621 | 	// Define test response for successful refund fetch
622 | 	successfulRefundsResp := map[string]interface{}{
623 | 		"entity": "collection",
624 | 		"count":  float64(2),
625 | 		"items": []interface{}{
626 | 			map[string]interface{}{
627 | 				"id":         "rfnd_FFX6AnnIN3puqW",
628 | 				"entity":     "refund",
629 | 				"amount":     float64(88800),
630 | 				"currency":   "INR",
631 | 				"payment_id": "pay_FFX5FdEYx8jPwA",
632 | 				"notes": map[string]interface{}{
633 | 					"comment": "Issuing an instant refund",
634 | 				},
635 | 				"receipt":         nil,
636 | 				"acquirer_data":   map[string]interface{}{},
637 | 				"created_at":      float64(1594982363),
638 | 				"batch_id":        nil,
639 | 				"status":          "processed",
640 | 				"speed_processed": "optimum",
641 | 				"speed_requested": "optimum",
642 | 			},
643 | 			map[string]interface{}{
644 | 				"id":         "rfnd_EqWThTE7dd7utf",
645 | 				"entity":     "refund",
646 | 				"amount":     float64(6000),
647 | 				"currency":   "INR",
648 | 				"payment_id": "pay_EpkFDYRirena0f",
649 | 				"notes": map[string]interface{}{
650 | 					"comment": "Issuing a normal refund",
651 | 				},
652 | 				"receipt": nil,
653 | 				"acquirer_data": map[string]interface{}{
654 | 					"arn": "10000000000000",
655 | 				},
656 | 				"created_at":      float64(1589521675),
657 | 				"batch_id":        nil,
658 | 				"status":          "processed",
659 | 				"speed_processed": "normal",
660 | 				"speed_requested": "normal",
661 | 			},
662 | 		},
663 | 	}
664 | 
665 | 	// Define error response
666 | 	errorResp := map[string]interface{}{
667 | 		"error": map[string]interface{}{
668 | 			"code":        "BAD_REQUEST_ERROR",
669 | 			"description": "Bad request",
670 | 		},
671 | 	}
672 | 
673 | 	tests := []RazorpayToolTestCase{
674 | 		{
675 | 			Name: "successful fetch with pagination parameters",
676 | 			Request: map[string]interface{}{
677 | 				"count": 2,
678 | 				"skip":  1,
679 | 				"from":  1589000000,
680 | 				"to":    1595000000,
681 | 			},
682 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
683 | 				return mock.NewHTTPClient(
684 | 					mock.Endpoint{
685 | 						Path:     fetchAllRefundsPath,
686 | 						Method:   "GET",
687 | 						Response: successfulRefundsResp,
688 | 					},
689 | 				)
690 | 			},
691 | 			ExpectError:    false,
692 | 			ExpectedResult: successfulRefundsResp,
693 | 		},
694 | 		{
695 | 			Name:    "fetch with API error",
696 | 			Request: map[string]interface{}{},
697 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
698 | 				return mock.NewHTTPClient(
699 | 					mock.Endpoint{
700 | 						Path:     fetchAllRefundsPath,
701 | 						Method:   "GET",
702 | 						Response: errorResp,
703 | 					},
704 | 				)
705 | 			},
706 | 			ExpectError:    true,
707 | 			ExpectedErrMsg: "fetching refunds failed",
708 | 		},
709 | 		{
710 | 			Name: "multiple validation errors",
711 | 			Request: map[string]interface{}{
712 | 				"from":  "not-a-number", // Wrong type for from
713 | 				"to":    "not-a-number", // Wrong type for to
714 | 				"count": "not-a-number", // Wrong type for count
715 | 				"skip":  "not-a-number", // Wrong type for skip
716 | 			},
717 | 			MockHttpClient: nil,
718 | 			ExpectError:    true,
719 | 			ExpectedErrMsg: "Validation errors:\n- " +
720 | 				"invalid parameter type: from\n- " +
721 | 				"invalid parameter type: to\n- " +
722 | 				"invalid parameter type: count\n- " +
723 | 				"invalid parameter type: skip",
724 | 		},
725 | 	}
726 | 
727 | 	for _, tc := range tests {
728 | 		t.Run(tc.Name, func(t *testing.T) {
729 | 			runToolTest(t, tc, FetchAllRefunds, "Refund")
730 | 		})
731 | 	}
732 | }
733 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/settlements_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_FetchSettlement(t *testing.T) {
 15 | 	fetchSettlementPathFmt := fmt.Sprintf(
 16 | 		"/%s%s/%%s",
 17 | 		constants.VERSION_V1,
 18 | 		constants.SETTLEMENT_URL,
 19 | 	)
 20 | 
 21 | 	settlementResp := map[string]interface{}{
 22 | 		"id":         "setl_FNj7g2YS5J67Rz",
 23 | 		"entity":     "settlement",
 24 | 		"amount":     float64(9973635),
 25 | 		"status":     "processed",
 26 | 		"fees":       float64(471),
 27 | 		"tax":        float64(72),
 28 | 		"utr":        "1568176198",
 29 | 		"created_at": float64(1568176198),
 30 | 	}
 31 | 
 32 | 	settlementNotFoundResp := map[string]interface{}{
 33 | 		"error": map[string]interface{}{
 34 | 			"code":        "BAD_REQUEST_ERROR",
 35 | 			"description": "settlement not found",
 36 | 		},
 37 | 	}
 38 | 
 39 | 	tests := []RazorpayToolTestCase{
 40 | 		{
 41 | 			Name: "successful settlement fetch",
 42 | 			Request: map[string]interface{}{
 43 | 				"settlement_id": "setl_FNj7g2YS5J67Rz",
 44 | 			},
 45 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 46 | 				return mock.NewHTTPClient(
 47 | 					mock.Endpoint{
 48 | 						Path:     fmt.Sprintf(fetchSettlementPathFmt, "setl_FNj7g2YS5J67Rz"),
 49 | 						Method:   "GET",
 50 | 						Response: settlementResp,
 51 | 					},
 52 | 				)
 53 | 			},
 54 | 			ExpectError:    false,
 55 | 			ExpectedResult: settlementResp,
 56 | 		},
 57 | 		{
 58 | 			Name: "settlement not found",
 59 | 			Request: map[string]interface{}{
 60 | 				"settlement_id": "setl_invalid",
 61 | 			},
 62 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 63 | 				return mock.NewHTTPClient(
 64 | 					mock.Endpoint{
 65 | 						Path:     fmt.Sprintf(fetchSettlementPathFmt, "setl_invalid"),
 66 | 						Method:   "GET",
 67 | 						Response: settlementNotFoundResp,
 68 | 					},
 69 | 				)
 70 | 			},
 71 | 			ExpectError:    true,
 72 | 			ExpectedErrMsg: "fetching settlement failed: settlement not found",
 73 | 		},
 74 | 		{
 75 | 			Name:           "missing settlement_id parameter",
 76 | 			Request:        map[string]interface{}{},
 77 | 			MockHttpClient: nil, // No HTTP client needed for validation error
 78 | 			ExpectError:    true,
 79 | 			ExpectedErrMsg: "missing required parameter: settlement_id",
 80 | 		},
 81 | 	}
 82 | 
 83 | 	for _, tc := range tests {
 84 | 		t.Run(tc.Name, func(t *testing.T) {
 85 | 			runToolTest(t, tc, FetchSettlement, "Settlement")
 86 | 		})
 87 | 	}
 88 | }
 89 | 
 90 | func Test_FetchSettlementRecon(t *testing.T) {
 91 | 	fetchSettlementReconPath := fmt.Sprintf(
 92 | 		"/%s%s/recon/combined",
 93 | 		constants.VERSION_V1,
 94 | 		constants.SETTLEMENT_URL,
 95 | 	)
 96 | 
 97 | 	settlementReconResp := map[string]interface{}{
 98 | 		"entity": "collection",
 99 | 		"count":  float64(1),
100 | 		"items": []interface{}{
101 | 			map[string]interface{}{
102 | 				"entity":            "settlement",
103 | 				"settlement_id":     "setl_FNj7g2YS5J67Rz",
104 | 				"settlement_utr":    "1568176198",
105 | 				"amount":            float64(9973635),
106 | 				"settlement_type":   "regular",
107 | 				"settlement_status": "processed",
108 | 				"created_at":        float64(1568176198),
109 | 			},
110 | 		},
111 | 	}
112 | 
113 | 	invalidParamsResp := map[string]interface{}{
114 | 		"error": map[string]interface{}{
115 | 			"code":        "BAD_REQUEST_ERROR",
116 | 			"description": "missing required parameters",
117 | 		},
118 | 	}
119 | 
120 | 	tests := []RazorpayToolTestCase{
121 | 		{
122 | 			Name: "successful settlement reconciliation fetch",
123 | 			Request: map[string]interface{}{
124 | 				"year":  float64(2022),
125 | 				"month": float64(10),
126 | 				"day":   float64(15),
127 | 				"count": float64(10),
128 | 				"skip":  float64(0),
129 | 			},
130 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
131 | 				return mock.NewHTTPClient(
132 | 					mock.Endpoint{
133 | 						Path:     fetchSettlementReconPath,
134 | 						Method:   "GET",
135 | 						Response: settlementReconResp,
136 | 					},
137 | 				)
138 | 			},
139 | 			ExpectError:    false,
140 | 			ExpectedResult: settlementReconResp,
141 | 		},
142 | 		{
143 | 			Name: "settlement reconciliation with required params only",
144 | 			Request: map[string]interface{}{
145 | 				"year":  float64(2022),
146 | 				"month": float64(10),
147 | 			},
148 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
149 | 				return mock.NewHTTPClient(
150 | 					mock.Endpoint{
151 | 						Path:     fetchSettlementReconPath,
152 | 						Method:   "GET",
153 | 						Response: settlementReconResp,
154 | 					},
155 | 				)
156 | 			},
157 | 			ExpectError:    false,
158 | 			ExpectedResult: settlementReconResp,
159 | 		},
160 | 		{
161 | 			Name: "settlement reconciliation with invalid params",
162 | 			Request: map[string]interface{}{
163 | 				"year": float64(2022),
164 | 				// missing month parameter
165 | 			},
166 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
167 | 				return mock.NewHTTPClient(
168 | 					mock.Endpoint{
169 | 						Path:     fetchSettlementReconPath,
170 | 						Method:   "GET",
171 | 						Response: invalidParamsResp,
172 | 					},
173 | 				)
174 | 			},
175 | 			ExpectError:    true,
176 | 			ExpectedErrMsg: "missing required parameter: month",
177 | 		},
178 | 		{
179 | 			Name:           "missing required parameters",
180 | 			Request:        map[string]interface{}{},
181 | 			MockHttpClient: nil, // No HTTP client needed for validation error
182 | 			ExpectError:    true,
183 | 			ExpectedErrMsg: "missing required parameter: year",
184 | 		},
185 | 	}
186 | 
187 | 	for _, tc := range tests {
188 | 		t.Run(tc.Name, func(t *testing.T) {
189 | 			runToolTest(t, tc, FetchSettlementRecon, "Settlement Reconciliation")
190 | 		})
191 | 	}
192 | }
193 | 
194 | func Test_FetchAllSettlements(t *testing.T) {
195 | 	fetchAllSettlementsPath := fmt.Sprintf(
196 | 		"/%s%s",
197 | 		constants.VERSION_V1,
198 | 		constants.SETTLEMENT_URL,
199 | 	)
200 | 
201 | 	// Define the sample response for all settlements
202 | 	settlementsResp := map[string]interface{}{
203 | 		"entity": "collection",
204 | 		"count":  float64(2),
205 | 		"items": []interface{}{
206 | 			map[string]interface{}{
207 | 				"id":     "setl_FNj7g2YS5J67Rz",
208 | 				"entity": "settlement",
209 | 				"amount": float64(9973635),
210 | 				"status": "processed",
211 | 			},
212 | 			map[string]interface{}{
213 | 				"id":     "setl_FJOp0jOWlalIvt",
214 | 				"entity": "settlement",
215 | 				"amount": float64(299114),
216 | 				"status": "processed",
217 | 			},
218 | 		},
219 | 	}
220 | 
221 | 	invalidParamsResp := map[string]interface{}{
222 | 		"error": map[string]interface{}{
223 | 			"code":        "BAD_REQUEST_ERROR",
224 | 			"description": "from must be between 946684800 and 4765046400",
225 | 		},
226 | 	}
227 | 
228 | 	tests := []RazorpayToolTestCase{
229 | 		{
230 | 			Name:    "successful settlements fetch with no parameters",
231 | 			Request: map[string]interface{}{},
232 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
233 | 				return mock.NewHTTPClient(
234 | 					mock.Endpoint{
235 | 						Path:     fetchAllSettlementsPath,
236 | 						Method:   "GET",
237 | 						Response: settlementsResp,
238 | 					},
239 | 				)
240 | 			},
241 | 			ExpectError:    false,
242 | 			ExpectedResult: settlementsResp,
243 | 		},
244 | 		{
245 | 			Name: "successful settlements fetch with pagination",
246 | 			Request: map[string]interface{}{
247 | 				"count": float64(10),
248 | 				"skip":  float64(0),
249 | 			},
250 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
251 | 				return mock.NewHTTPClient(
252 | 					mock.Endpoint{
253 | 						Path:     fetchAllSettlementsPath,
254 | 						Method:   "GET",
255 | 						Response: settlementsResp,
256 | 					},
257 | 				)
258 | 			},
259 | 			ExpectError:    false,
260 | 			ExpectedResult: settlementsResp,
261 | 		},
262 | 		{
263 | 			Name: "successful settlements fetch with date range",
264 | 			Request: map[string]interface{}{
265 | 				"from": float64(1609459200), // 2021-01-01
266 | 				"to":   float64(1640995199), // 2021-12-31
267 | 			},
268 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
269 | 				return mock.NewHTTPClient(
270 | 					mock.Endpoint{
271 | 						Path:     fetchAllSettlementsPath,
272 | 						Method:   "GET",
273 | 						Response: settlementsResp,
274 | 					},
275 | 				)
276 | 			},
277 | 			ExpectError:    false,
278 | 			ExpectedResult: settlementsResp,
279 | 		},
280 | 		{
281 | 			Name: "settlements fetch with invalid timestamp",
282 | 			Request: map[string]interface{}{
283 | 				"from": float64(900000000), // Invalid timestamp (too early)
284 | 				"to":   float64(1600000000),
285 | 			},
286 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
287 | 				return mock.NewHTTPClient(
288 | 					mock.Endpoint{
289 | 						Path:     fetchAllSettlementsPath,
290 | 						Method:   "GET",
291 | 						Response: invalidParamsResp,
292 | 					},
293 | 				)
294 | 			},
295 | 			ExpectError: true,
296 | 			ExpectedErrMsg: "fetching settlements failed: from must be " +
297 | 				"between 946684800 and 4765046400",
298 | 		},
299 | 	}
300 | 
301 | 	for _, tc := range tests {
302 | 		t.Run(tc.Name, func(t *testing.T) {
303 | 			runToolTest(t, tc, FetchAllSettlements, "Settlements List")
304 | 		})
305 | 	}
306 | }
307 | 
308 | func Test_CreateInstantSettlement(t *testing.T) {
309 | 	createInstantSettlementPath := fmt.Sprintf(
310 | 		"/%s%s/ondemand",
311 | 		constants.VERSION_V1,
312 | 		constants.SETTLEMENT_URL,
313 | 	)
314 | 
315 | 	// Successful response with all parameters
316 | 	successfulSettlementResp := map[string]interface{}{
317 | 		"id":                  "setlod_FNj7g2YS5J67Rz",
318 | 		"entity":              "settlement.ondemand",
319 | 		"amount_requested":    float64(200000),
320 | 		"amount_settled":      float64(0),
321 | 		"amount_pending":      float64(199410),
322 | 		"amount_reversed":     float64(0),
323 | 		"fees":                float64(590),
324 | 		"tax":                 float64(90),
325 | 		"currency":            "INR",
326 | 		"settle_full_balance": false,
327 | 		"status":              "initiated",
328 | 		"description":         "Need this to make vendor payments.",
329 | 		"notes": map[string]interface{}{
330 | 			"notes_key_1": "Tea, Earl Grey, Hot",
331 | 			"notes_key_2": "Tea, Earl Grey… decaf.",
332 | 		},
333 | 		"created_at": float64(1596771429),
334 | 	}
335 | 
336 | 	// Error response for insufficient amount
337 | 	insufficientAmountResp := map[string]interface{}{
338 | 		"error": map[string]interface{}{
339 | 			"code":        "BAD_REQUEST_ERROR",
340 | 			"description": "Minimum amount that can be settled is ₹ 1.",
341 | 		},
342 | 	}
343 | 
344 | 	tests := []RazorpayToolTestCase{
345 | 		{
346 | 			Name: "successful settlement creation with all parameters",
347 | 			Request: map[string]interface{}{
348 | 				"amount":              float64(200000),
349 | 				"settle_full_balance": false,
350 | 				"description":         "Need this to make vendor payments.",
351 | 				"notes": map[string]interface{}{
352 | 					"notes_key_1": "Tea, Earl Grey, Hot",
353 | 					"notes_key_2": "Tea, Earl Grey… decaf.",
354 | 				},
355 | 			},
356 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
357 | 				return mock.NewHTTPClient(
358 | 					mock.Endpoint{
359 | 						Path:     createInstantSettlementPath,
360 | 						Method:   "POST",
361 | 						Response: successfulSettlementResp,
362 | 					},
363 | 				)
364 | 			},
365 | 			ExpectError:    false,
366 | 			ExpectedResult: successfulSettlementResp,
367 | 		},
368 | 		{
369 | 			Name: "settlement creation with required parameters only",
370 | 			Request: map[string]interface{}{
371 | 				"amount": float64(200000),
372 | 			},
373 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
374 | 				return mock.NewHTTPClient(
375 | 					mock.Endpoint{
376 | 						Path:     createInstantSettlementPath,
377 | 						Method:   "POST",
378 | 						Response: successfulSettlementResp,
379 | 					},
380 | 				)
381 | 			},
382 | 			ExpectError:    false,
383 | 			ExpectedResult: successfulSettlementResp,
384 | 		},
385 | 		{
386 | 			Name: "settlement creation with insufficient amount",
387 | 			Request: map[string]interface{}{
388 | 				"amount": float64(10), // Less than minimum
389 | 			},
390 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
391 | 				return mock.NewHTTPClient(
392 | 					mock.Endpoint{
393 | 						Path:     createInstantSettlementPath,
394 | 						Method:   "POST",
395 | 						Response: insufficientAmountResp,
396 | 					},
397 | 				)
398 | 			},
399 | 			ExpectError: true,
400 | 			ExpectedErrMsg: "creating instant settlement failed: Minimum amount that " +
401 | 				"can be settled is ₹ 1.",
402 | 		},
403 | 		{
404 | 			Name:           "missing amount parameter",
405 | 			Request:        map[string]interface{}{},
406 | 			MockHttpClient: nil, // No HTTP client needed for validation error
407 | 			ExpectError:    true,
408 | 			ExpectedErrMsg: "missing required parameter: amount",
409 | 		},
410 | 	}
411 | 
412 | 	for _, tc := range tests {
413 | 		t.Run(tc.Name, func(t *testing.T) {
414 | 			runToolTest(t, tc, CreateInstantSettlement, "Instant Settlement")
415 | 		})
416 | 	}
417 | }
418 | 
419 | func Test_FetchAllInstantSettlements(t *testing.T) {
420 | 	fetchAllInstantSettlementsPath := fmt.Sprintf(
421 | 		"/%s%s/ondemand",
422 | 		constants.VERSION_V1,
423 | 		constants.SETTLEMENT_URL,
424 | 	)
425 | 
426 | 	// Sample response for successful fetch without expanded payouts
427 | 	basicSettlementListResp := map[string]interface{}{
428 | 		"entity": "collection",
429 | 		"count":  float64(2),
430 | 		"items": []interface{}{
431 | 			map[string]interface{}{
432 | 				"id":                  "setlod_FNj7g2YS5J67Rz",
433 | 				"entity":              "settlement.ondemand",
434 | 				"amount_requested":    float64(200000),
435 | 				"amount_settled":      float64(199410),
436 | 				"amount_pending":      float64(0),
437 | 				"amount_reversed":     float64(0),
438 | 				"fees":                float64(590),
439 | 				"tax":                 float64(90),
440 | 				"currency":            "INR",
441 | 				"settle_full_balance": false,
442 | 				"status":              "processed",
443 | 				"description":         "Need this to make vendor payments.",
444 | 				"notes": map[string]interface{}{
445 | 					"notes_key_1": "Tea, Earl Grey, Hot",
446 | 					"notes_key_2": "Tea, Earl Grey… decaf.",
447 | 				},
448 | 				"created_at": float64(1596771429),
449 | 			},
450 | 			map[string]interface{}{
451 | 				"id":                  "setlod_FJOp0jOWlalIvt",
452 | 				"entity":              "settlement.ondemand",
453 | 				"amount_requested":    float64(300000),
454 | 				"amount_settled":      float64(299114),
455 | 				"amount_pending":      float64(0),
456 | 				"amount_reversed":     float64(0),
457 | 				"fees":                float64(886),
458 | 				"tax":                 float64(136),
459 | 				"currency":            "INR",
460 | 				"settle_full_balance": false,
461 | 				"status":              "processed",
462 | 				"description":         "Need this to buy stock.",
463 | 				"notes": map[string]interface{}{
464 | 					"notes_key_1": "Tea, Earl Grey, Hot",
465 | 					"notes_key_2": "Tea, Earl Grey… decaf.",
466 | 				},
467 | 				"created_at": float64(1595826576),
468 | 			},
469 | 		},
470 | 	}
471 | 
472 | 	// Sample response with expanded payouts
473 | 	expandedSettlementListResp := map[string]interface{}{
474 | 		"entity": "collection",
475 | 		"count":  float64(2),
476 | 		"items": []interface{}{
477 | 			map[string]interface{}{
478 | 				"id":                  "setlod_FNj7g2YS5J67Rz",
479 | 				"entity":              "settlement.ondemand",
480 | 				"amount_requested":    float64(200000),
481 | 				"amount_settled":      float64(199410),
482 | 				"amount_pending":      float64(0),
483 | 				"amount_reversed":     float64(0),
484 | 				"fees":                float64(590),
485 | 				"tax":                 float64(90),
486 | 				"currency":            "INR",
487 | 				"settle_full_balance": false,
488 | 				"status":              "processed",
489 | 				"description":         "Need this to make vendor payments.",
490 | 				"notes": map[string]interface{}{
491 | 					"notes_key_1": "Tea, Earl Grey, Hot",
492 | 					"notes_key_2": "Tea, Earl Grey… decaf.",
493 | 				},
494 | 				"created_at": float64(1596771429),
495 | 				"ondemand_payouts": []interface{}{
496 | 					map[string]interface{}{
497 | 						"id":     "pout_FNj7g2YS5J67Rz",
498 | 						"entity": "payout",
499 | 						"amount": float64(199410),
500 | 						"status": "processed",
501 | 					},
502 | 				},
503 | 			},
504 | 			map[string]interface{}{
505 | 				"id":                  "setlod_FJOp0jOWlalIvt",
506 | 				"entity":              "settlement.ondemand",
507 | 				"amount_requested":    float64(300000),
508 | 				"amount_settled":      float64(299114),
509 | 				"amount_pending":      float64(0),
510 | 				"amount_reversed":     float64(0),
511 | 				"fees":                float64(886),
512 | 				"tax":                 float64(136),
513 | 				"currency":            "INR",
514 | 				"settle_full_balance": false,
515 | 				"status":              "processed",
516 | 				"description":         "Need this to buy stock.",
517 | 				"notes": map[string]interface{}{
518 | 					"notes_key_1": "Tea, Earl Grey, Hot",
519 | 					"notes_key_2": "Tea, Earl Grey… decaf.",
520 | 				},
521 | 				"created_at": float64(1595826576),
522 | 				"ondemand_payouts": []interface{}{
523 | 					map[string]interface{}{
524 | 						"id":     "pout_FJOp0jOWlalIvt",
525 | 						"entity": "payout",
526 | 						"amount": float64(299114),
527 | 						"status": "processed",
528 | 					},
529 | 				},
530 | 			},
531 | 		},
532 | 	}
533 | 
534 | 	// Error response when parameters are invalid
535 | 	invalidParamsResp := map[string]interface{}{
536 | 		"error": map[string]interface{}{
537 | 			"code":        "BAD_REQUEST_ERROR",
538 | 			"description": "from must be between 946684800 and 4765046400",
539 | 		},
540 | 	}
541 | 
542 | 	tests := []RazorpayToolTestCase{
543 | 		{
544 | 			Name:    "successful instant settlements fetch with no parameters",
545 | 			Request: map[string]interface{}{},
546 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
547 | 				return mock.NewHTTPClient(
548 | 					mock.Endpoint{
549 | 						Path:     fetchAllInstantSettlementsPath,
550 | 						Method:   "GET",
551 | 						Response: basicSettlementListResp,
552 | 					},
553 | 				)
554 | 			},
555 | 			ExpectError:    false,
556 | 			ExpectedResult: basicSettlementListResp,
557 | 		},
558 | 		{
559 | 			Name: "instant settlements fetch with pagination",
560 | 			Request: map[string]interface{}{
561 | 				"count": float64(10),
562 | 				"skip":  float64(0),
563 | 			},
564 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
565 | 				return mock.NewHTTPClient(
566 | 					mock.Endpoint{
567 | 						Path:     fetchAllInstantSettlementsPath,
568 | 						Method:   "GET",
569 | 						Response: basicSettlementListResp,
570 | 					},
571 | 				)
572 | 			},
573 | 			ExpectError:    false,
574 | 			ExpectedResult: basicSettlementListResp,
575 | 		},
576 | 		{
577 | 			Name: "instant settlements fetch with expanded payouts",
578 | 			Request: map[string]interface{}{
579 | 				"expand": []interface{}{"ondemand_payouts"},
580 | 			},
581 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
582 | 				return mock.NewHTTPClient(
583 | 					mock.Endpoint{
584 | 						Path:     fetchAllInstantSettlementsPath,
585 | 						Method:   "GET",
586 | 						Response: expandedSettlementListResp,
587 | 					},
588 | 				)
589 | 			},
590 | 			ExpectError:    false,
591 | 			ExpectedResult: expandedSettlementListResp,
592 | 		},
593 | 		{
594 | 			Name: "instant settlements fetch with date range",
595 | 			Request: map[string]interface{}{
596 | 				"from": float64(1609459200), // 2021-01-01
597 | 				"to":   float64(1640995199), // 2021-12-31
598 | 			},
599 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
600 | 				return mock.NewHTTPClient(
601 | 					mock.Endpoint{
602 | 						Path:     fetchAllInstantSettlementsPath,
603 | 						Method:   "GET",
604 | 						Response: basicSettlementListResp,
605 | 					},
606 | 				)
607 | 			},
608 | 			ExpectError:    false,
609 | 			ExpectedResult: basicSettlementListResp,
610 | 		},
611 | 		{
612 | 			Name: "instant settlements fetch with invalid timestamp",
613 | 			Request: map[string]interface{}{
614 | 				"from": float64(900000000), // Invalid timestamp (too early)
615 | 				"to":   float64(1600000000),
616 | 			},
617 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
618 | 				return mock.NewHTTPClient(
619 | 					mock.Endpoint{
620 | 						Path:     fetchAllInstantSettlementsPath,
621 | 						Method:   "GET",
622 | 						Response: invalidParamsResp,
623 | 					},
624 | 				)
625 | 			},
626 | 			ExpectError: true,
627 | 			ExpectedErrMsg: "fetching instant settlements failed: from must be " +
628 | 				"between 946684800 and 4765046400",
629 | 		},
630 | 	}
631 | 
632 | 	for _, tc := range tests {
633 | 		t.Run(tc.Name, func(t *testing.T) {
634 | 			runToolTest(t, tc, FetchAllInstantSettlements, "Instant Settlements List")
635 | 		})
636 | 	}
637 | }
638 | 
639 | func Test_FetchInstantSettlement(t *testing.T) {
640 | 	fetchInstantSettlementPathFmt := fmt.Sprintf(
641 | 		"/%s%s/ondemand/%%s",
642 | 		constants.VERSION_V1,
643 | 		constants.SETTLEMENT_URL,
644 | 	)
645 | 
646 | 	instantSettlementResp := map[string]interface{}{
647 | 		"id":                  "setlod_FNj7g2YS5J67Rz",
648 | 		"entity":              "settlement.ondemand",
649 | 		"amount_requested":    float64(200000),
650 | 		"amount_settled":      float64(199410),
651 | 		"amount_pending":      float64(0),
652 | 		"amount_reversed":     float64(0),
653 | 		"fees":                float64(590),
654 | 		"tax":                 float64(90),
655 | 		"currency":            "INR",
656 | 		"settle_full_balance": false,
657 | 		"status":              "processed",
658 | 		"description":         "Need this to make vendor payments.",
659 | 		"notes": map[string]interface{}{
660 | 			"notes_key_1": "Tea, Earl Grey, Hot",
661 | 			"notes_key_2": "Tea, Earl Grey… decaf.",
662 | 		},
663 | 		"created_at": float64(1596771429),
664 | 	}
665 | 
666 | 	instantSettlementNotFoundResp := map[string]interface{}{
667 | 		"error": map[string]interface{}{
668 | 			"code":        "BAD_REQUEST_ERROR",
669 | 			"description": "instant settlement not found",
670 | 		},
671 | 	}
672 | 
673 | 	tests := []RazorpayToolTestCase{
674 | 		{
675 | 			Name: "successful instant settlement fetch",
676 | 			Request: map[string]interface{}{
677 | 				"settlement_id": "setlod_FNj7g2YS5J67Rz",
678 | 			},
679 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
680 | 				return mock.NewHTTPClient(
681 | 					mock.Endpoint{
682 | 						Path: fmt.Sprintf(fetchInstantSettlementPathFmt,
683 | 							"setlod_FNj7g2YS5J67Rz"),
684 | 						Method:   "GET",
685 | 						Response: instantSettlementResp,
686 | 					},
687 | 				)
688 | 			},
689 | 			ExpectError:    false,
690 | 			ExpectedResult: instantSettlementResp,
691 | 		},
692 | 		{
693 | 			Name: "instant settlement not found",
694 | 			Request: map[string]interface{}{
695 | 				"settlement_id": "setlod_invalid",
696 | 			},
697 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
698 | 				return mock.NewHTTPClient(
699 | 					mock.Endpoint{
700 | 						Path:     fmt.Sprintf(fetchInstantSettlementPathFmt, "setlod_invalid"),
701 | 						Method:   "GET",
702 | 						Response: instantSettlementNotFoundResp,
703 | 					},
704 | 				)
705 | 			},
706 | 			ExpectError: true,
707 | 			ExpectedErrMsg: "fetching instant settlement failed: " +
708 | 				"instant settlement not found",
709 | 		},
710 | 		{
711 | 			Name:           "missing settlement_id parameter",
712 | 			Request:        map[string]interface{}{},
713 | 			MockHttpClient: nil, // No HTTP client needed for validation error
714 | 			ExpectError:    true,
715 | 			ExpectedErrMsg: "missing required parameter: settlement_id",
716 | 		},
717 | 	}
718 | 
719 | 	for _, tc := range tests {
720 | 		t.Run(tc.Name, func(t *testing.T) {
721 | 			runToolTest(t, tc, FetchInstantSettlement, "Instant Settlement")
722 | 		})
723 | 	}
724 | }
725 | 
```

--------------------------------------------------------------------------------
/pkg/mcpgo/tool_test.go:
--------------------------------------------------------------------------------

```go
  1 | package mcpgo
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 	"testing"
  7 | 
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | 	"github.com/stretchr/testify/assert"
 10 | )
 11 | 
 12 | func TestNewTool(t *testing.T) {
 13 | 	t.Run("creates tool with all fields", func(t *testing.T) {
 14 | 		handler := func(
 15 | 			ctx context.Context, req CallToolRequest) (*ToolResult, error) {
 16 | 			return NewToolResultText("success"), nil
 17 | 		}
 18 | 		tool := NewTool(
 19 | 			"test-tool",
 20 | 			"Test description",
 21 | 			[]ToolParameter{WithString("param1")},
 22 | 			handler,
 23 | 		)
 24 | 		assert.NotNil(t, tool)
 25 | 		assert.NotNil(t, tool.GetHandler())
 26 | 	})
 27 | 
 28 | 	t.Run("creates tool with empty parameters", func(t *testing.T) {
 29 | 		handler := func(
 30 | 			ctx context.Context, req CallToolRequest) (*ToolResult, error) {
 31 | 			return NewToolResultText("success"), nil
 32 | 		}
 33 | 		tool := NewTool("test-tool", "Test", []ToolParameter{}, handler)
 34 | 		assert.NotNil(t, tool)
 35 | 	})
 36 | }
 37 | 
 38 | func TestMark3labsToolImpl_GetHandler(t *testing.T) {
 39 | 	t.Run("returns handler", func(t *testing.T) {
 40 | 		handler := func(
 41 | 			ctx context.Context, req CallToolRequest) (*ToolResult, error) {
 42 | 			return NewToolResultText("success"), nil
 43 | 		}
 44 | 		tool := NewTool("test-tool", "Test", []ToolParameter{}, handler)
 45 | 		assert.NotNil(t, tool.GetHandler())
 46 | 	})
 47 | }
 48 | 
 49 | func TestMark3labsToolImpl_ToMCPServerTool(t *testing.T) {
 50 | 	t.Run("converts tool with string parameter", func(t *testing.T) {
 51 | 		tool := NewTool(
 52 | 			"test-tool",
 53 | 			"Test",
 54 | 			[]ToolParameter{WithString("param1")},
 55 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
 56 | 				return NewToolResultText("success"), nil
 57 | 			},
 58 | 		)
 59 | 		mcpTool := tool.toMCPServerTool()
 60 | 		assert.NotNil(t, mcpTool.Tool)
 61 | 		assert.NotNil(t, mcpTool.Handler)
 62 | 	})
 63 | 
 64 | 	t.Run("converts tool with number parameter", func(t *testing.T) {
 65 | 		tool := NewTool(
 66 | 			"test-tool",
 67 | 			"Test",
 68 | 			[]ToolParameter{WithNumber("param1")},
 69 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
 70 | 				return NewToolResultText("success"), nil
 71 | 			},
 72 | 		)
 73 | 		mcpTool := tool.toMCPServerTool()
 74 | 		assert.NotNil(t, mcpTool.Tool)
 75 | 	})
 76 | 
 77 | 	t.Run("converts tool with boolean parameter", func(t *testing.T) {
 78 | 		tool := NewTool(
 79 | 			"test-tool",
 80 | 			"Test",
 81 | 			[]ToolParameter{WithBoolean("param1")},
 82 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
 83 | 				return NewToolResultText("success"), nil
 84 | 			},
 85 | 		)
 86 | 		mcpTool := tool.toMCPServerTool()
 87 | 		assert.NotNil(t, mcpTool.Tool)
 88 | 	})
 89 | 
 90 | 	t.Run("converts tool with object parameter", func(t *testing.T) {
 91 | 		tool := NewTool(
 92 | 			"test-tool",
 93 | 			"Test",
 94 | 			[]ToolParameter{WithObject("param1")},
 95 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
 96 | 				return NewToolResultText("success"), nil
 97 | 			},
 98 | 		)
 99 | 		mcpTool := tool.toMCPServerTool()
100 | 		assert.NotNil(t, mcpTool.Tool)
101 | 	})
102 | 
103 | 	t.Run("converts tool with array parameter", func(t *testing.T) {
104 | 		tool := NewTool(
105 | 			"test-tool",
106 | 			"Test",
107 | 			[]ToolParameter{WithArray("param1")},
108 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
109 | 				return NewToolResultText("success"), nil
110 | 			},
111 | 		)
112 | 		mcpTool := tool.toMCPServerTool()
113 | 		assert.NotNil(t, mcpTool.Tool)
114 | 	})
115 | 
116 | 	t.Run("converts tool with integer parameter", func(t *testing.T) {
117 | 		param := ToolParameter{
118 | 			Name:   "param1",
119 | 			Schema: map[string]interface{}{"type": "integer"},
120 | 		}
121 | 		tool := NewTool(
122 | 			"test-tool",
123 | 			"Test",
124 | 			[]ToolParameter{param},
125 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
126 | 				return NewToolResultText("success"), nil
127 | 			},
128 | 		)
129 | 		mcpTool := tool.toMCPServerTool()
130 | 		assert.NotNil(t, mcpTool.Tool)
131 | 	})
132 | 
133 | 	t.Run("converts tool with unknown type parameter", func(t *testing.T) {
134 | 		param := ToolParameter{
135 | 			Name:   "param1",
136 | 			Schema: map[string]interface{}{"type": "unknown"},
137 | 		}
138 | 		tool := NewTool(
139 | 			"test-tool",
140 | 			"Test",
141 | 			[]ToolParameter{param},
142 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
143 | 				return NewToolResultText("success"), nil
144 | 			},
145 | 		)
146 | 		mcpTool := tool.toMCPServerTool()
147 | 		assert.NotNil(t, mcpTool.Tool)
148 | 	})
149 | 
150 | 	t.Run("converts tool with missing type parameter", func(t *testing.T) {
151 | 		param := ToolParameter{
152 | 			Name:   "param1",
153 | 			Schema: map[string]interface{}{},
154 | 		}
155 | 		tool := NewTool(
156 | 			"test-tool",
157 | 			"Test",
158 | 			[]ToolParameter{param},
159 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
160 | 				return NewToolResultText("success"), nil
161 | 			},
162 | 		)
163 | 		mcpTool := tool.toMCPServerTool()
164 | 		assert.NotNil(t, mcpTool.Tool)
165 | 	})
166 | 
167 | 	t.Run("converts tool with non-string type", func(t *testing.T) {
168 | 		param := ToolParameter{
169 | 			Name:   "param1",
170 | 			Schema: map[string]interface{}{"type": 123},
171 | 		}
172 | 		tool := NewTool(
173 | 			"test-tool",
174 | 			"Test",
175 | 			[]ToolParameter{param},
176 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
177 | 				return NewToolResultText("success"), nil
178 | 			},
179 | 		)
180 | 		mcpTool := tool.toMCPServerTool()
181 | 		assert.NotNil(t, mcpTool.Tool)
182 | 	})
183 | 
184 | 	t.Run("handler returns error result", func(t *testing.T) {
185 | 		tool := NewTool(
186 | 			"test-tool",
187 | 			"Test",
188 | 			[]ToolParameter{},
189 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
190 | 				return NewToolResultError("error occurred"), nil
191 | 			},
192 | 		)
193 | 		mcpTool := tool.toMCPServerTool()
194 | 		assert.NotNil(t, mcpTool.Handler)
195 | 
196 | 		req := mcp.CallToolRequest{
197 | 			Params: mcp.CallToolParams{
198 | 				Name:      "test-tool",
199 | 				Arguments: map[string]interface{}{},
200 | 			},
201 | 		}
202 | 		result, err := mcpTool.Handler(context.Background(), req)
203 | 		assert.NoError(t, err)
204 | 		assert.NotNil(t, result)
205 | 	})
206 | 
207 | 	t.Run("handler returns handler error", func(t *testing.T) {
208 | 		tool := NewTool(
209 | 			"test-tool",
210 | 			"Test",
211 | 			[]ToolParameter{},
212 | 			func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
213 | 				return nil, assert.AnError
214 | 			},
215 | 		)
216 | 		mcpTool := tool.toMCPServerTool()
217 | 		assert.NotNil(t, mcpTool.Handler)
218 | 
219 | 		req := mcp.CallToolRequest{
220 | 			Params: mcp.CallToolParams{
221 | 				Name:      "test-tool",
222 | 				Arguments: map[string]interface{}{},
223 | 			},
224 | 		}
225 | 		result, err := mcpTool.Handler(context.Background(), req)
226 | 		assert.Error(t, err)
227 | 		assert.Nil(t, result)
228 | 	})
229 | }
230 | 
231 | func TestPropertyOption_Min(t *testing.T) {
232 | 	t.Run("sets minimum for number", func(t *testing.T) {
233 | 		schema := map[string]interface{}{"type": "number"}
234 | 		Min(10.0)(schema)
235 | 		assert.Equal(t, 10.0, schema["minimum"])
236 | 	})
237 | 
238 | 	t.Run("sets minimum for integer", func(t *testing.T) {
239 | 		schema := map[string]interface{}{"type": "integer"}
240 | 		Min(5.0)(schema)
241 | 		assert.Equal(t, 5.0, schema["minimum"])
242 | 	})
243 | 
244 | 	t.Run("sets minLength for string", func(t *testing.T) {
245 | 		schema := map[string]interface{}{"type": "string"}
246 | 		Min(3.0)(schema)
247 | 		assert.Equal(t, 3, schema["minLength"])
248 | 	})
249 | 
250 | 	t.Run("sets minItems for array", func(t *testing.T) {
251 | 		schema := map[string]interface{}{"type": "array"}
252 | 		Min(2.0)(schema)
253 | 		assert.Equal(t, 2, schema["minItems"])
254 | 	})
255 | 
256 | 	t.Run("ignores for unknown type", func(t *testing.T) {
257 | 		schema := map[string]interface{}{"type": "boolean"}
258 | 		Min(1.0)(schema)
259 | 		assert.NotContains(t, schema, "minimum")
260 | 		assert.NotContains(t, schema, "minLength")
261 | 		assert.NotContains(t, schema, "minItems")
262 | 	})
263 | 
264 | 	t.Run("ignores for missing type", func(t *testing.T) {
265 | 		schema := map[string]interface{}{}
266 | 		Min(1.0)(schema)
267 | 		assert.NotContains(t, schema, "minimum")
268 | 	})
269 | 
270 | 	t.Run("ignores for non-string type", func(t *testing.T) {
271 | 		schema := map[string]interface{}{"type": 123}
272 | 		Min(1.0)(schema)
273 | 		assert.NotContains(t, schema, "minimum")
274 | 	})
275 | }
276 | 
277 | func TestPropertyOption_Max(t *testing.T) {
278 | 	t.Run("sets maximum for number", func(t *testing.T) {
279 | 		schema := map[string]interface{}{"type": "number"}
280 | 		Max(100.0)(schema)
281 | 		assert.Equal(t, 100.0, schema["maximum"])
282 | 	})
283 | 
284 | 	t.Run("sets maximum for integer", func(t *testing.T) {
285 | 		schema := map[string]interface{}{"type": "integer"}
286 | 		Max(50.0)(schema)
287 | 		assert.Equal(t, 50.0, schema["maximum"])
288 | 	})
289 | 
290 | 	t.Run("sets maxLength for string", func(t *testing.T) {
291 | 		schema := map[string]interface{}{"type": "string"}
292 | 		Max(10.0)(schema)
293 | 		assert.Equal(t, 10, schema["maxLength"])
294 | 	})
295 | 
296 | 	t.Run("sets maxItems for array", func(t *testing.T) {
297 | 		schema := map[string]interface{}{"type": "array"}
298 | 		Max(5.0)(schema)
299 | 		assert.Equal(t, 5, schema["maxItems"])
300 | 	})
301 | 
302 | 	t.Run("ignores for unknown type", func(t *testing.T) {
303 | 		schema := map[string]interface{}{"type": "boolean"}
304 | 		Max(1.0)(schema)
305 | 		assert.NotContains(t, schema, "maximum")
306 | 	})
307 | 
308 | 	t.Run("ignores for missing type", func(t *testing.T) {
309 | 		schema := map[string]interface{}{}
310 | 		Max(1.0)(schema)
311 | 		assert.NotContains(t, schema, "maximum")
312 | 	})
313 | 
314 | 	t.Run("ignores for non-string type value", func(t *testing.T) {
315 | 		schema := map[string]interface{}{"type": 123}
316 | 		Max(1.0)(schema)
317 | 		assert.NotContains(t, schema, "maximum")
318 | 	})
319 | }
320 | 
321 | func TestPropertyOption_Pattern(t *testing.T) {
322 | 	t.Run("sets pattern for string", func(t *testing.T) {
323 | 		schema := map[string]interface{}{"type": "string"}
324 | 		Pattern("^[a-z]+$")(schema)
325 | 		assert.Equal(t, "^[a-z]+$", schema["pattern"])
326 | 	})
327 | 
328 | 	t.Run("ignores for non-string type", func(t *testing.T) {
329 | 		schema := map[string]interface{}{"type": "number"}
330 | 		Pattern("^[a-z]+$")(schema)
331 | 		assert.NotContains(t, schema, "pattern")
332 | 	})
333 | 
334 | 	t.Run("ignores for missing type", func(t *testing.T) {
335 | 		schema := map[string]interface{}{}
336 | 		Pattern("^[a-z]+$")(schema)
337 | 		assert.NotContains(t, schema, "pattern")
338 | 	})
339 | 
340 | 	t.Run("ignores for non-string type value", func(t *testing.T) {
341 | 		schema := map[string]interface{}{"type": 123}
342 | 		Pattern("^[a-z]+$")(schema)
343 | 		assert.NotContains(t, schema, "pattern")
344 | 	})
345 | }
346 | 
347 | func TestPropertyOption_Enum(t *testing.T) {
348 | 	t.Run("sets enum values", func(t *testing.T) {
349 | 		schema := map[string]interface{}{}
350 | 		Enum("value1", "value2", "value3")(schema)
351 | 		assert.Equal(t, []interface{}{"value1", "value2", "value3"}, schema["enum"])
352 | 	})
353 | 
354 | 	t.Run("sets enum with mixed types", func(t *testing.T) {
355 | 		schema := map[string]interface{}{}
356 | 		Enum("value1", 123, true)(schema)
357 | 		assert.Equal(t, []interface{}{"value1", 123, true}, schema["enum"])
358 | 	})
359 | }
360 | 
361 | func TestPropertyOption_DefaultValue(t *testing.T) {
362 | 	t.Run("sets default string value", func(t *testing.T) {
363 | 		schema := map[string]interface{}{}
364 | 		DefaultValue("default")(schema)
365 | 		assert.Equal(t, "default", schema["default"])
366 | 	})
367 | 
368 | 	t.Run("sets default number value", func(t *testing.T) {
369 | 		schema := map[string]interface{}{}
370 | 		DefaultValue(42.0)(schema)
371 | 		assert.Equal(t, 42.0, schema["default"])
372 | 	})
373 | 
374 | 	t.Run("sets default boolean value", func(t *testing.T) {
375 | 		schema := map[string]interface{}{}
376 | 		DefaultValue(true)(schema)
377 | 		assert.Equal(t, true, schema["default"])
378 | 	})
379 | }
380 | 
381 | func TestPropertyOption_MaxProperties(t *testing.T) {
382 | 	t.Run("sets maxProperties for object", func(t *testing.T) {
383 | 		schema := map[string]interface{}{"type": "object"}
384 | 		MaxProperties(5)(schema)
385 | 		assert.Equal(t, 5, schema["maxProperties"])
386 | 	})
387 | 
388 | 	t.Run("ignores for non-object type", func(t *testing.T) {
389 | 		schema := map[string]interface{}{"type": "string"}
390 | 		MaxProperties(5)(schema)
391 | 		assert.NotContains(t, schema, "maxProperties")
392 | 	})
393 | 
394 | 	t.Run("ignores for missing type", func(t *testing.T) {
395 | 		schema := map[string]interface{}{}
396 | 		MaxProperties(5)(schema)
397 | 		assert.NotContains(t, schema, "maxProperties")
398 | 	})
399 | }
400 | 
401 | func TestPropertyOption_MinProperties(t *testing.T) {
402 | 	t.Run("sets minProperties for object", func(t *testing.T) {
403 | 		schema := map[string]interface{}{"type": "object"}
404 | 		MinProperties(2)(schema)
405 | 		assert.Equal(t, 2, schema["minProperties"])
406 | 	})
407 | 
408 | 	t.Run("ignores for non-object type", func(t *testing.T) {
409 | 		schema := map[string]interface{}{"type": "string"}
410 | 		MinProperties(2)(schema)
411 | 		assert.NotContains(t, schema, "minProperties")
412 | 	})
413 | }
414 | 
415 | func TestPropertyOption_Required(t *testing.T) {
416 | 	t.Run("sets required flag", func(t *testing.T) {
417 | 		schema := map[string]interface{}{}
418 | 		Required()(schema)
419 | 		assert.Equal(t, true, schema["required"])
420 | 	})
421 | }
422 | 
423 | func TestPropertyOption_Description(t *testing.T) {
424 | 	t.Run("sets description", func(t *testing.T) {
425 | 		schema := map[string]interface{}{}
426 | 		Description("Test description")(schema)
427 | 		assert.Equal(t, "Test description", schema["description"])
428 | 	})
429 | }
430 | 
431 | func TestToolParameter_ApplyPropertyOptions(t *testing.T) {
432 | 	t.Run("applies single option", func(t *testing.T) {
433 | 		param := ToolParameter{
434 | 			Name:   "test",
435 | 			Schema: map[string]interface{}{"type": "string"},
436 | 		}
437 | 		param.applyPropertyOptions(Description("Test desc"))
438 | 		assert.Equal(t, "Test desc", param.Schema["description"])
439 | 	})
440 | 
441 | 	t.Run("applies multiple options", func(t *testing.T) {
442 | 		param := ToolParameter{
443 | 			Name:   "test",
444 | 			Schema: map[string]interface{}{"type": "string"},
445 | 		}
446 | 		param.applyPropertyOptions(
447 | 			Description("Test desc"),
448 | 			Required(),
449 | 			Min(3.0),
450 | 		)
451 | 		assert.Equal(t, "Test desc", param.Schema["description"])
452 | 		assert.Equal(t, true, param.Schema["required"])
453 | 		assert.Equal(t, 3, param.Schema["minLength"])
454 | 	})
455 | 
456 | 	t.Run("applies no options", func(t *testing.T) {
457 | 		param := ToolParameter{
458 | 			Name:   "test",
459 | 			Schema: map[string]interface{}{"type": "string"},
460 | 		}
461 | 		param.applyPropertyOptions()
462 | 		assert.Equal(t, "string", param.Schema["type"])
463 | 	})
464 | }
465 | 
466 | func TestWithString(t *testing.T) {
467 | 	t.Run("creates string parameter without options", func(t *testing.T) {
468 | 		param := WithString("test")
469 | 		assert.Equal(t, "test", param.Name)
470 | 		assert.Equal(t, "string", param.Schema["type"])
471 | 	})
472 | 
473 | 	t.Run("creates string parameter with options", func(t *testing.T) {
474 | 		param := WithString("test", Description("Test"), Required(), Min(3.0))
475 | 		assert.Equal(t, "test", param.Name)
476 | 		assert.Equal(t, "string", param.Schema["type"])
477 | 		assert.Equal(t, "Test", param.Schema["description"])
478 | 		assert.Equal(t, true, param.Schema["required"])
479 | 		assert.Equal(t, 3, param.Schema["minLength"])
480 | 	})
481 | }
482 | 
483 | func TestWithNumber(t *testing.T) {
484 | 	t.Run("creates number parameter without options", func(t *testing.T) {
485 | 		param := WithNumber("test")
486 | 		assert.Equal(t, "test", param.Name)
487 | 		assert.Equal(t, "number", param.Schema["type"])
488 | 	})
489 | 
490 | 	t.Run("creates number parameter with options", func(t *testing.T) {
491 | 		param := WithNumber("test", Min(1.0), Max(100.0))
492 | 		assert.Equal(t, "test", param.Name)
493 | 		assert.Equal(t, "number", param.Schema["type"])
494 | 		assert.Equal(t, 1.0, param.Schema["minimum"])
495 | 		assert.Equal(t, 100.0, param.Schema["maximum"])
496 | 	})
497 | }
498 | 
499 | func TestWithBoolean(t *testing.T) {
500 | 	t.Run("creates boolean parameter", func(t *testing.T) {
501 | 		param := WithBoolean("test")
502 | 		assert.Equal(t, "test", param.Name)
503 | 		assert.Equal(t, "boolean", param.Schema["type"])
504 | 	})
505 | }
506 | 
507 | func TestWithObject(t *testing.T) {
508 | 	t.Run("creates object parameter", func(t *testing.T) {
509 | 		param := WithObject("test")
510 | 		assert.Equal(t, "test", param.Name)
511 | 		assert.Equal(t, "object", param.Schema["type"])
512 | 	})
513 | 
514 | 	t.Run("creates object parameter with options", func(t *testing.T) {
515 | 		param := WithObject("test", MinProperties(1), MaxProperties(5))
516 | 		assert.Equal(t, "test", param.Name)
517 | 		assert.Equal(t, "object", param.Schema["type"])
518 | 		assert.Equal(t, 1, param.Schema["minProperties"])
519 | 		assert.Equal(t, 5, param.Schema["maxProperties"])
520 | 	})
521 | }
522 | 
523 | func TestWithArray(t *testing.T) {
524 | 	t.Run("creates array parameter", func(t *testing.T) {
525 | 		param := WithArray("test")
526 | 		assert.Equal(t, "test", param.Name)
527 | 		assert.Equal(t, "array", param.Schema["type"])
528 | 	})
529 | 
530 | 	t.Run("creates array parameter with options", func(t *testing.T) {
531 | 		param := WithArray("test", Min(1.0), Max(10.0))
532 | 		assert.Equal(t, "test", param.Name)
533 | 		assert.Equal(t, "array", param.Schema["type"])
534 | 		assert.Equal(t, 1, param.Schema["minItems"])
535 | 		assert.Equal(t, 10, param.Schema["maxItems"])
536 | 	})
537 | }
538 | 
539 | func TestAddNumberPropertyOptions(t *testing.T) {
540 | 	t.Run("adds minimum", func(t *testing.T) {
541 | 		schema := map[string]interface{}{"minimum": 10.0}
542 | 		opts := addNumberPropertyOptions(nil, schema)
543 | 		assert.NotNil(t, opts)
544 | 	})
545 | 
546 | 	t.Run("adds maximum", func(t *testing.T) {
547 | 		schema := map[string]interface{}{"maximum": 100.0}
548 | 		opts := addNumberPropertyOptions(nil, schema)
549 | 		assert.NotNil(t, opts)
550 | 	})
551 | 
552 | 	t.Run("adds both minimum and maximum", func(t *testing.T) {
553 | 		schema := map[string]interface{}{
554 | 			"minimum": 10.0,
555 | 			"maximum": 100.0,
556 | 		}
557 | 		opts := addNumberPropertyOptions(nil, schema)
558 | 		assert.NotNil(t, opts)
559 | 	})
560 | 
561 | 	t.Run("handles non-float64 minimum", func(t *testing.T) {
562 | 		schema := map[string]interface{}{"minimum": "not-a-number"}
563 | 		opts := addNumberPropertyOptions(nil, schema)
564 | 		assert.Nil(t, opts)
565 | 	})
566 | }
567 | 
568 | func TestAddStringPropertyOptions(t *testing.T) {
569 | 	t.Run("adds minLength", func(t *testing.T) {
570 | 		schema := map[string]interface{}{"minLength": 3}
571 | 		opts := addStringPropertyOptions(nil, schema)
572 | 		assert.NotNil(t, opts)
573 | 	})
574 | 
575 | 	t.Run("adds maxLength", func(t *testing.T) {
576 | 		schema := map[string]interface{}{"maxLength": 10}
577 | 		opts := addStringPropertyOptions(nil, schema)
578 | 		assert.NotNil(t, opts)
579 | 	})
580 | 
581 | 	t.Run("adds pattern", func(t *testing.T) {
582 | 		schema := map[string]interface{}{"pattern": "^[a-z]+$"}
583 | 		opts := addStringPropertyOptions(nil, schema)
584 | 		assert.NotNil(t, opts)
585 | 	})
586 | 
587 | 	t.Run("adds all string options", func(t *testing.T) {
588 | 		schema := map[string]interface{}{
589 | 			"minLength": 3,
590 | 			"maxLength": 10,
591 | 			"pattern":   "^[a-z]+$",
592 | 		}
593 | 		opts := addStringPropertyOptions(nil, schema)
594 | 		assert.NotNil(t, opts)
595 | 	})
596 | }
597 | 
598 | func TestAddDefaultValueOptions(t *testing.T) {
599 | 	t.Run("adds string default", func(t *testing.T) {
600 | 		opts := addDefaultValueOptions(nil, "default")
601 | 		assert.NotNil(t, opts)
602 | 	})
603 | 
604 | 	t.Run("adds float64 default", func(t *testing.T) {
605 | 		opts := addDefaultValueOptions(nil, 42.0)
606 | 		assert.NotNil(t, opts)
607 | 	})
608 | 
609 | 	t.Run("adds bool default", func(t *testing.T) {
610 | 		opts := addDefaultValueOptions(nil, true)
611 | 		assert.NotNil(t, opts)
612 | 	})
613 | 
614 | 	t.Run("ignores unknown type", func(t *testing.T) {
615 | 		opts := addDefaultValueOptions(nil, []string{"test"})
616 | 		assert.Nil(t, opts)
617 | 	})
618 | }
619 | 
620 | func TestAddEnumOptions(t *testing.T) {
621 | 	t.Run("adds enum with string values", func(t *testing.T) {
622 | 		enumValues := []interface{}{"value1", "value2", "value3"}
623 | 		opts := addEnumOptions(nil, enumValues)
624 | 		assert.NotNil(t, opts)
625 | 	})
626 | 
627 | 	t.Run("adds enum with mixed values", func(t *testing.T) {
628 | 		enumValues := []interface{}{"value1", 123, "value2"}
629 | 		opts := addEnumOptions(nil, enumValues)
630 | 		assert.NotNil(t, opts)
631 | 	})
632 | 
633 | 	t.Run("handles non-array enum", func(t *testing.T) {
634 | 		opts := addEnumOptions(nil, "not-an-array")
635 | 		assert.Nil(t, opts)
636 | 	})
637 | 
638 | 	t.Run("handles empty enum array", func(t *testing.T) {
639 | 		enumValues := []interface{}{123, 456} // Non-string values
640 | 		opts := addEnumOptions(nil, enumValues)
641 | 		assert.Nil(t, opts) // Should return nil since no string values
642 | 	})
643 | }
644 | 
645 | func TestAddObjectPropertyOptions(t *testing.T) {
646 | 	t.Run("adds maxProperties", func(t *testing.T) {
647 | 		schema := map[string]interface{}{"maxProperties": 5}
648 | 		opts := addObjectPropertyOptions(nil, schema)
649 | 		assert.NotNil(t, opts)
650 | 	})
651 | 
652 | 	t.Run("adds minProperties", func(t *testing.T) {
653 | 		schema := map[string]interface{}{"minProperties": 2}
654 | 		opts := addObjectPropertyOptions(nil, schema)
655 | 		assert.NotNil(t, opts)
656 | 	})
657 | 
658 | 	t.Run("adds both properties", func(t *testing.T) {
659 | 		schema := map[string]interface{}{
660 | 			"minProperties": 1,
661 | 			"maxProperties": 5,
662 | 		}
663 | 		opts := addObjectPropertyOptions(nil, schema)
664 | 		assert.NotNil(t, opts)
665 | 	})
666 | }
667 | 
668 | func TestAddArrayPropertyOptions(t *testing.T) {
669 | 	t.Run("adds minItems", func(t *testing.T) {
670 | 		schema := map[string]interface{}{"minItems": 1}
671 | 		opts := addArrayPropertyOptions(nil, schema)
672 | 		assert.NotNil(t, opts)
673 | 	})
674 | 
675 | 	t.Run("adds maxItems", func(t *testing.T) {
676 | 		schema := map[string]interface{}{"maxItems": 10}
677 | 		opts := addArrayPropertyOptions(nil, schema)
678 | 		assert.NotNil(t, opts)
679 | 	})
680 | 
681 | 	t.Run("adds both items", func(t *testing.T) {
682 | 		schema := map[string]interface{}{
683 | 			"minItems": 1,
684 | 			"maxItems": 10,
685 | 		}
686 | 		opts := addArrayPropertyOptions(nil, schema)
687 | 		assert.NotNil(t, opts)
688 | 	})
689 | }
690 | 
691 | func TestConvertSchemaToPropertyOptions(t *testing.T) {
692 | 	t.Run("converts complete schema", func(t *testing.T) {
693 | 		schema := map[string]interface{}{
694 | 			"type":        "string",
695 | 			"description": "Test param",
696 | 			"required":    true,
697 | 			"minLength":   3,
698 | 			"maxLength":   10,
699 | 			"pattern":     "^[a-z]+$",
700 | 			"default":     "default",
701 | 		}
702 | 		opts := convertSchemaToPropertyOptions(schema)
703 | 		assert.NotNil(t, opts)
704 | 	})
705 | 
706 | 	t.Run("converts number schema", func(t *testing.T) {
707 | 		schema := map[string]interface{}{
708 | 			"type":    "number",
709 | 			"minimum": 1.0,
710 | 			"maximum": 100.0,
711 | 			"default": 42.0,
712 | 		}
713 | 		opts := convertSchemaToPropertyOptions(schema)
714 | 		assert.NotNil(t, opts)
715 | 	})
716 | 
717 | 	t.Run("converts object schema", func(t *testing.T) {
718 | 		schema := map[string]interface{}{
719 | 			"type":          "object",
720 | 			"minProperties": 1,
721 | 			"maxProperties": 5,
722 | 		}
723 | 		opts := convertSchemaToPropertyOptions(schema)
724 | 		assert.NotNil(t, opts)
725 | 	})
726 | 
727 | 	t.Run("converts array schema", func(t *testing.T) {
728 | 		schema := map[string]interface{}{
729 | 			"type":     "array",
730 | 			"minItems": 1,
731 | 			"maxItems": 10,
732 | 		}
733 | 		opts := convertSchemaToPropertyOptions(schema)
734 | 		assert.NotNil(t, opts)
735 | 	})
736 | 
737 | 	t.Run("converts schema with enum", func(t *testing.T) {
738 | 		schema := map[string]interface{}{
739 | 			"type": "string",
740 | 			"enum": []interface{}{"value1", "value2"},
741 | 		}
742 | 		opts := convertSchemaToPropertyOptions(schema)
743 | 		assert.NotNil(t, opts)
744 | 	})
745 | 
746 | 	t.Run("handles empty description", func(t *testing.T) {
747 | 		schema := map[string]interface{}{
748 | 			"type":        "string",
749 | 			"description": "",
750 | 		}
751 | 		opts := convertSchemaToPropertyOptions(schema)
752 | 		// Empty description should not be added
753 | 		// In Go, a nil slice is valid and has length 0
754 | 		assert.Len(t, opts, 0)
755 | 	})
756 | 
757 | 	t.Run("handles false required", func(t *testing.T) {
758 | 		schema := map[string]interface{}{
759 | 			"type":     "string",
760 | 			"required": false,
761 | 		}
762 | 		opts := convertSchemaToPropertyOptions(schema)
763 | 		// False required should not be added
764 | 		// In Go, a nil slice is valid and has length 0
765 | 		assert.Len(t, opts, 0)
766 | 	})
767 | }
768 | 
769 | func TestNewToolResultJSON(t *testing.T) {
770 | 	t.Run("creates JSON result from map", func(t *testing.T) {
771 | 		data := map[string]interface{}{
772 | 			"key": "value",
773 | 			"num": 42,
774 | 		}
775 | 		result, err := NewToolResultJSON(data)
776 | 		assert.NoError(t, err)
777 | 		assert.NotNil(t, result)
778 | 		assert.False(t, result.IsError)
779 | 		assert.NotEmpty(t, result.Text)
780 | 
781 | 		// Verify it's valid JSON
782 | 		var decoded map[string]interface{}
783 | 		err = json.Unmarshal([]byte(result.Text), &decoded)
784 | 		assert.NoError(t, err)
785 | 		assert.Equal(t, "value", decoded["key"])
786 | 	})
787 | 
788 | 	t.Run("creates JSON result from struct", func(t *testing.T) {
789 | 		type TestStruct struct {
790 | 			Name string `json:"name"`
791 | 			Age  int    `json:"age"`
792 | 		}
793 | 		data := TestStruct{Name: "Test", Age: 30}
794 | 		result, err := NewToolResultJSON(data)
795 | 		assert.NoError(t, err)
796 | 		assert.NotNil(t, result)
797 | 		assert.False(t, result.IsError)
798 | 	})
799 | 
800 | 	t.Run("creates JSON result from array", func(t *testing.T) {
801 | 		data := []string{"item1", "item2"}
802 | 		result, err := NewToolResultJSON(data)
803 | 		assert.NoError(t, err)
804 | 		assert.NotNil(t, result)
805 | 	})
806 | 
807 | 	t.Run("handles unmarshalable data", func(t *testing.T) {
808 | 		// Create a channel which cannot be marshaled to JSON
809 | 		data := make(chan int)
810 | 		result, err := NewToolResultJSON(data)
811 | 		assert.Error(t, err)
812 | 		assert.Nil(t, result)
813 | 	})
814 | }
815 | 
816 | func TestNewToolResultText(t *testing.T) {
817 | 	t.Run("creates text result", func(t *testing.T) {
818 | 		result := NewToolResultText("test text")
819 | 		assert.NotNil(t, result)
820 | 		assert.Equal(t, "test text", result.Text)
821 | 		assert.False(t, result.IsError)
822 | 		assert.Nil(t, result.Content)
823 | 	})
824 | 
825 | 	t.Run("creates empty text result", func(t *testing.T) {
826 | 		result := NewToolResultText("")
827 | 		assert.NotNil(t, result)
828 | 		assert.Equal(t, "", result.Text)
829 | 		assert.False(t, result.IsError)
830 | 	})
831 | }
832 | 
833 | func TestNewToolResultError(t *testing.T) {
834 | 	t.Run("creates error result", func(t *testing.T) {
835 | 		result := NewToolResultError("error message")
836 | 		assert.NotNil(t, result)
837 | 		assert.Equal(t, "error message", result.Text)
838 | 		assert.True(t, result.IsError)
839 | 		assert.Nil(t, result.Content)
840 | 	})
841 | 
842 | 	t.Run("creates empty error result", func(t *testing.T) {
843 | 		result := NewToolResultError("")
844 | 		assert.NotNil(t, result)
845 | 		assert.Equal(t, "", result.Text)
846 | 		assert.True(t, result.IsError)
847 | 	})
848 | }
849 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/qr_codes_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_CreateQRCode(t *testing.T) {
 15 | 	createQRCodePath := fmt.Sprintf(
 16 | 		"/%s%s",
 17 | 		constants.VERSION_V1,
 18 | 		constants.QRCODE_URL,
 19 | 	)
 20 | 
 21 | 	qrCodeWithAllParamsResp := map[string]interface{}{
 22 | 		"id":                       "qr_HMsVL8HOpbMcjU",
 23 | 		"entity":                   "qr_code",
 24 | 		"created_at":               float64(1623660301),
 25 | 		"name":                     "Store Front Display",
 26 | 		"usage":                    "single_use",
 27 | 		"type":                     "upi_qr",
 28 | 		"image_url":                "https://rzp.io/i/BWcUVrLp",
 29 | 		"payment_amount":           float64(300),
 30 | 		"status":                   "active",
 31 | 		"description":              "For Store 1",
 32 | 		"fixed_amount":             true,
 33 | 		"payments_amount_received": float64(0),
 34 | 		"payments_count_received":  float64(0),
 35 | 		"notes": map[string]interface{}{
 36 | 			"purpose": "Test UPI QR Code notes",
 37 | 		},
 38 | 		"customer_id": "cust_HKsR5se84c5LTO",
 39 | 		"close_by":    float64(1681615838),
 40 | 	}
 41 | 
 42 | 	qrCodeWithRequiredParamsResp := map[string]interface{}{
 43 | 		"id":                       "qr_HMsVL8HOpbMcjU",
 44 | 		"entity":                   "qr_code",
 45 | 		"created_at":               float64(1623660301),
 46 | 		"usage":                    "multiple_use",
 47 | 		"type":                     "upi_qr",
 48 | 		"image_url":                "https://rzp.io/i/BWcUVrLp",
 49 | 		"status":                   "active",
 50 | 		"fixed_amount":             false,
 51 | 		"payments_amount_received": float64(0),
 52 | 		"payments_count_received":  float64(0),
 53 | 	}
 54 | 
 55 | 	qrCodeWithoutPaymentAmountResp := map[string]interface{}{
 56 | 		"id":                       "qr_HMsVL8HOpbMcjU",
 57 | 		"entity":                   "qr_code",
 58 | 		"created_at":               float64(1623660301),
 59 | 		"name":                     "Store Front Display",
 60 | 		"usage":                    "single_use",
 61 | 		"type":                     "upi_qr",
 62 | 		"image_url":                "https://rzp.io/i/BWcUVrLp",
 63 | 		"status":                   "active",
 64 | 		"description":              "For Store 1",
 65 | 		"fixed_amount":             false,
 66 | 		"payments_amount_received": float64(0),
 67 | 		"payments_count_received":  float64(0),
 68 | 	}
 69 | 
 70 | 	errorResp := map[string]interface{}{
 71 | 		"error": map[string]interface{}{
 72 | 			"code":        "BAD_REQUEST_ERROR",
 73 | 			"description": "The type field is invalid",
 74 | 		},
 75 | 	}
 76 | 
 77 | 	tests := []RazorpayToolTestCase{
 78 | 		{
 79 | 			Name: "successful QR code creation with all parameters",
 80 | 			Request: map[string]interface{}{
 81 | 				"type":           "upi_qr",
 82 | 				"name":           "Store Front Display",
 83 | 				"usage":          "single_use",
 84 | 				"fixed_amount":   true,
 85 | 				"payment_amount": float64(300),
 86 | 				"description":    "For Store 1",
 87 | 				"customer_id":    "cust_HKsR5se84c5LTO",
 88 | 				"close_by":       float64(1681615838),
 89 | 				"notes": map[string]interface{}{
 90 | 					"purpose": "Test UPI QR Code notes",
 91 | 				},
 92 | 			},
 93 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
 94 | 				return mock.NewHTTPClient(
 95 | 					mock.Endpoint{
 96 | 						Path:     createQRCodePath,
 97 | 						Method:   "POST",
 98 | 						Response: qrCodeWithAllParamsResp,
 99 | 					},
100 | 				)
101 | 			},
102 | 			ExpectError:    false,
103 | 			ExpectedResult: qrCodeWithAllParamsResp,
104 | 		},
105 | 		{
106 | 			Name: "successful QR code creation with required params only",
107 | 			Request: map[string]interface{}{
108 | 				"type":  "upi_qr",
109 | 				"usage": "multiple_use",
110 | 			},
111 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
112 | 				return mock.NewHTTPClient(
113 | 					mock.Endpoint{
114 | 						Path:     createQRCodePath,
115 | 						Method:   "POST",
116 | 						Response: qrCodeWithRequiredParamsResp,
117 | 					},
118 | 				)
119 | 			},
120 | 			ExpectError:    false,
121 | 			ExpectedResult: qrCodeWithRequiredParamsResp,
122 | 		},
123 | 		{
124 | 			Name: "successful QR code creation without payment amount",
125 | 			Request: map[string]interface{}{
126 | 				"type":         "upi_qr",
127 | 				"name":         "Store Front Display",
128 | 				"usage":        "single_use",
129 | 				"fixed_amount": false,
130 | 				"description":  "For Store 1",
131 | 			},
132 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
133 | 				return mock.NewHTTPClient(
134 | 					mock.Endpoint{
135 | 						Path:     createQRCodePath,
136 | 						Method:   "POST",
137 | 						Response: qrCodeWithoutPaymentAmountResp,
138 | 					},
139 | 				)
140 | 			},
141 | 			ExpectError:    false,
142 | 			ExpectedResult: qrCodeWithoutPaymentAmountResp,
143 | 		},
144 | 		{
145 | 			Name: "missing required type parameter",
146 | 			Request: map[string]interface{}{
147 | 				"usage": "single_use",
148 | 			},
149 | 			MockHttpClient: nil,
150 | 			ExpectError:    true,
151 | 			ExpectedErrMsg: "missing required parameter: type",
152 | 		},
153 | 		{
154 | 			Name: "missing required usage parameter",
155 | 			Request: map[string]interface{}{
156 | 				"type": "upi_qr",
157 | 			},
158 | 			MockHttpClient: nil,
159 | 			ExpectError:    true,
160 | 			ExpectedErrMsg: "missing required parameter: usage",
161 | 		},
162 | 		{
163 | 			Name: "validator error - invalid parameter type",
164 | 			Request: map[string]interface{}{
165 | 				"type":  123,
166 | 				"usage": "single_use",
167 | 			},
168 | 			MockHttpClient: nil,
169 | 			ExpectError:    true,
170 | 			ExpectedErrMsg: "Validation errors",
171 | 		},
172 | 		{
173 | 			Name: "fixed_amount true but payment_amount missing",
174 | 			Request: map[string]interface{}{
175 | 				"type":         "upi_qr",
176 | 				"usage":        "single_use",
177 | 				"fixed_amount": true,
178 | 			},
179 | 			MockHttpClient: nil,
180 | 			ExpectError:    true,
181 | 			ExpectedErrMsg: "payment_amount is required when fixed_amount is true",
182 | 		},
183 | 		{
184 | 			Name: "invalid type parameter",
185 | 			Request: map[string]interface{}{
186 | 				"type":  "invalid_type",
187 | 				"usage": "single_use",
188 | 			},
189 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
190 | 				return mock.NewHTTPClient(
191 | 					mock.Endpoint{
192 | 						Path:     createQRCodePath,
193 | 						Method:   "POST",
194 | 						Response: errorResp,
195 | 					},
196 | 				)
197 | 			},
198 | 			ExpectError:    true,
199 | 			ExpectedErrMsg: "creating QR code failed: The type field is invalid",
200 | 		},
201 | 	}
202 | 
203 | 	for _, tc := range tests {
204 | 		t.Run(tc.Name, func(t *testing.T) {
205 | 			runToolTest(t, tc, CreateQRCode, "QR Code")
206 | 		})
207 | 	}
208 | }
209 | 
210 | func Test_FetchAllQRCodes(t *testing.T) {
211 | 	qrCodesPath := fmt.Sprintf(
212 | 		"/%s%s",
213 | 		constants.VERSION_V1,
214 | 		constants.QRCODE_URL,
215 | 	)
216 | 
217 | 	allQRCodesResp := map[string]interface{}{
218 | 		"entity": "collection",
219 | 		"count":  float64(2),
220 | 		"items": []interface{}{
221 | 			map[string]interface{}{
222 | 				"id":                       "qr_HO2jGkWReVBMNu",
223 | 				"entity":                   "qr_code",
224 | 				"created_at":               float64(1623914648),
225 | 				"name":                     "Store_1",
226 | 				"usage":                    "single_use",
227 | 				"type":                     "upi_qr",
228 | 				"image_url":                "https://rzp.io/i/w2CEwYmkAu",
229 | 				"payment_amount":           float64(300),
230 | 				"status":                   "active",
231 | 				"description":              "For Store 1",
232 | 				"fixed_amount":             true,
233 | 				"payments_amount_received": float64(0),
234 | 				"payments_count_received":  float64(0),
235 | 				"notes": map[string]interface{}{
236 | 					"purpose": "Test UPI QR Code notes",
237 | 				},
238 | 				"customer_id":  "cust_HKsR5se84c5LTO",
239 | 				"close_by":     float64(1681615838),
240 | 				"closed_at":    nil,
241 | 				"close_reason": nil,
242 | 			},
243 | 			map[string]interface{}{
244 | 				"id":                       "qr_HO2e0813YlchUn",
245 | 				"entity":                   "qr_code",
246 | 				"created_at":               float64(1623914349),
247 | 				"name":                     "Acme Groceries",
248 | 				"usage":                    "multiple_use",
249 | 				"type":                     "upi_qr",
250 | 				"image_url":                "https://rzp.io/i/X6QM7LL",
251 | 				"payment_amount":           nil,
252 | 				"status":                   "closed",
253 | 				"description":              "Buy fresh groceries",
254 | 				"fixed_amount":             false,
255 | 				"payments_amount_received": float64(200),
256 | 				"payments_count_received":  float64(1),
257 | 				"notes": map[string]interface{}{
258 | 					"Branch": "Bangalore - Rajaji Nagar",
259 | 				},
260 | 				"customer_id":  "cust_HKsR5se84c5LTO",
261 | 				"close_by":     float64(1625077799),
262 | 				"closed_at":    float64(1623914515),
263 | 				"close_reason": "on_demand",
264 | 			},
265 | 		},
266 | 	}
267 | 
268 | 	errorResp := map[string]interface{}{
269 | 		"error": map[string]interface{}{
270 | 			"code":        "BAD_REQUEST_ERROR",
271 | 			"description": "The query parameters are invalid",
272 | 		},
273 | 	}
274 | 
275 | 	tests := []RazorpayToolTestCase{
276 | 		{
277 | 			Name:    "successful fetch all QR codes with no parameters",
278 | 			Request: map[string]interface{}{},
279 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
280 | 				return mock.NewHTTPClient(
281 | 					mock.Endpoint{
282 | 						Path:     qrCodesPath,
283 | 						Method:   "GET",
284 | 						Response: allQRCodesResp,
285 | 					},
286 | 				)
287 | 			},
288 | 			ExpectError:    false,
289 | 			ExpectedResult: allQRCodesResp,
290 | 		},
291 | 		{
292 | 			Name: "successful fetch all QR codes with count parameter",
293 | 			Request: map[string]interface{}{
294 | 				"count": float64(2),
295 | 			},
296 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
297 | 				return mock.NewHTTPClient(
298 | 					mock.Endpoint{
299 | 						Path:     qrCodesPath,
300 | 						Method:   "GET",
301 | 						Response: allQRCodesResp,
302 | 					},
303 | 				)
304 | 			},
305 | 			ExpectError:    false,
306 | 			ExpectedResult: allQRCodesResp,
307 | 		},
308 | 		{
309 | 			Name: "successful fetch all QR codes with pagination parameters",
310 | 			Request: map[string]interface{}{
311 | 				"from":  float64(1622000000),
312 | 				"to":    float64(1625000000),
313 | 				"count": float64(2),
314 | 				"skip":  float64(0),
315 | 			},
316 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
317 | 				return mock.NewHTTPClient(
318 | 					mock.Endpoint{
319 | 						Path:     qrCodesPath,
320 | 						Method:   "GET",
321 | 						Response: allQRCodesResp,
322 | 					},
323 | 				)
324 | 			},
325 | 			ExpectError:    false,
326 | 			ExpectedResult: allQRCodesResp,
327 | 		},
328 | 		{
329 | 			Name: "invalid parameters - caught by SDK",
330 | 			Request: map[string]interface{}{
331 | 				"count": float64(-1),
332 | 			},
333 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
334 | 				return mock.NewHTTPClient(
335 | 					mock.Endpoint{
336 | 						Path:   qrCodesPath,
337 | 						Method: "GET",
338 | 						Response: map[string]interface{}{
339 | 							"error": map[string]interface{}{
340 | 								"code":        "BAD_REQUEST_ERROR",
341 | 								"description": "The count value should be greater than or equal to 1",
342 | 							},
343 | 						},
344 | 					},
345 | 				)
346 | 			},
347 | 			ExpectError: true,
348 | 			ExpectedErrMsg: "fetching QR codes failed: " +
349 | 				"The count value should be greater than or equal to 1",
350 | 		},
351 | 		{
352 | 			Name: "validator error - invalid count parameter type",
353 | 			Request: map[string]interface{}{
354 | 				"count": "not-a-number",
355 | 			},
356 | 			MockHttpClient: nil,
357 | 			ExpectError:    true,
358 | 			ExpectedErrMsg: "Validation errors",
359 | 		},
360 | 		{
361 | 			Name: "API error response",
362 | 			Request: map[string]interface{}{
363 | 				"count": float64(1000),
364 | 			},
365 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
366 | 				return mock.NewHTTPClient(
367 | 					mock.Endpoint{
368 | 						Path:     qrCodesPath,
369 | 						Method:   "GET",
370 | 						Response: errorResp,
371 | 					},
372 | 				)
373 | 			},
374 | 			ExpectError:    true,
375 | 			ExpectedErrMsg: "fetching QR codes failed: The query parameters are invalid",
376 | 		},
377 | 	}
378 | 
379 | 	for _, tc := range tests {
380 | 		t.Run(tc.Name, func(t *testing.T) {
381 | 			runToolTest(t, tc, FetchAllQRCodes, "QR Codes")
382 | 		})
383 | 	}
384 | }
385 | 
386 | func Test_FetchQRCodesByCustomerID(t *testing.T) {
387 | 	qrCodesPath := fmt.Sprintf(
388 | 		"/%s%s",
389 | 		constants.VERSION_V1,
390 | 		constants.QRCODE_URL,
391 | 	)
392 | 
393 | 	customerQRCodesResp := map[string]interface{}{
394 | 		"entity": "collection",
395 | 		"count":  float64(1),
396 | 		"items": []interface{}{
397 | 			map[string]interface{}{
398 | 				"id":                       "qr_HMsgvioW64f0vh",
399 | 				"entity":                   "qr_code",
400 | 				"created_at":               float64(1623660959),
401 | 				"name":                     "Store_1",
402 | 				"usage":                    "single_use",
403 | 				"type":                     "upi_qr",
404 | 				"image_url":                "https://rzp.io/i/DTa2eQR",
405 | 				"payment_amount":           float64(300),
406 | 				"status":                   "active",
407 | 				"description":              "For Store 1",
408 | 				"fixed_amount":             true,
409 | 				"payments_amount_received": float64(0),
410 | 				"payments_count_received":  float64(0),
411 | 				"notes": map[string]interface{}{
412 | 					"purpose": "Test UPI QR Code notes",
413 | 				},
414 | 				"customer_id":  "cust_HKsR5se84c5LTO",
415 | 				"close_by":     float64(1681615838),
416 | 				"closed_at":    nil,
417 | 				"close_reason": nil,
418 | 			},
419 | 		},
420 | 	}
421 | 
422 | 	errorResp := map[string]interface{}{
423 | 		"error": map[string]interface{}{
424 | 			"code":        "BAD_REQUEST_ERROR",
425 | 			"description": "The id provided is not a valid id",
426 | 		},
427 | 	}
428 | 
429 | 	tests := []RazorpayToolTestCase{
430 | 		{
431 | 			Name: "successful fetch QR codes by customer ID",
432 | 			Request: map[string]interface{}{
433 | 				"customer_id": "cust_HKsR5se84c5LTO",
434 | 			},
435 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
436 | 				return mock.NewHTTPClient(
437 | 					mock.Endpoint{
438 | 						Path:     qrCodesPath,
439 | 						Method:   "GET",
440 | 						Response: customerQRCodesResp,
441 | 					},
442 | 				)
443 | 			},
444 | 			ExpectError:    false,
445 | 			ExpectedResult: customerQRCodesResp,
446 | 		},
447 | 		{
448 | 			Name:           "missing required customer_id parameter",
449 | 			Request:        map[string]interface{}{},
450 | 			MockHttpClient: nil,
451 | 			ExpectError:    true,
452 | 			ExpectedErrMsg: "missing required parameter: customer_id",
453 | 		},
454 | 		{
455 | 			Name: "validator error - invalid customer_id parameter type",
456 | 			Request: map[string]interface{}{
457 | 				"customer_id": 12345,
458 | 			},
459 | 			MockHttpClient: nil,
460 | 			ExpectError:    true,
461 | 			ExpectedErrMsg: "invalid parameter type: customer_id",
462 | 		},
463 | 		{
464 | 			Name: "API error - invalid customer ID",
465 | 			Request: map[string]interface{}{
466 | 				"customer_id": "invalid_customer_id",
467 | 			},
468 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
469 | 				return mock.NewHTTPClient(
470 | 					mock.Endpoint{
471 | 						Path:     qrCodesPath,
472 | 						Method:   "GET",
473 | 						Response: errorResp,
474 | 					},
475 | 				)
476 | 			},
477 | 			ExpectError: true,
478 | 			ExpectedErrMsg: "fetching QR codes failed: " +
479 | 				"The id provided is not a valid id",
480 | 		},
481 | 	}
482 | 
483 | 	for _, tc := range tests {
484 | 		t.Run(tc.Name, func(t *testing.T) {
485 | 			runToolTest(t, tc, FetchQRCodesByCustomerID, "QR Codes by Customer ID")
486 | 		})
487 | 	}
488 | }
489 | 
490 | func Test_FetchQRCodesByPaymentID(t *testing.T) {
491 | 	qrCodesPath := fmt.Sprintf(
492 | 		"/%s%s",
493 | 		constants.VERSION_V1,
494 | 		constants.QRCODE_URL,
495 | 	)
496 | 
497 | 	paymentQRCodesResp := map[string]interface{}{
498 | 		"entity": "collection",
499 | 		"count":  float64(1),
500 | 		"items": []interface{}{
501 | 			map[string]interface{}{
502 | 				"id":                       "qr_HMsqRoeVwKbwAF",
503 | 				"entity":                   "qr_code",
504 | 				"created_at":               float64(1623661499),
505 | 				"name":                     "Fresh Groceries",
506 | 				"usage":                    "multiple_use",
507 | 				"type":                     "upi_qr",
508 | 				"image_url":                "https://rzp.io/i/eI9XD54Q",
509 | 				"payment_amount":           nil,
510 | 				"status":                   "active",
511 | 				"description":              "Buy fresh groceries",
512 | 				"fixed_amount":             false,
513 | 				"payments_amount_received": float64(1000),
514 | 				"payments_count_received":  float64(1),
515 | 				"notes":                    []interface{}{},
516 | 				"customer_id":              "cust_HKsR5se84c5LTO",
517 | 				"close_by":                 float64(1624472999),
518 | 				"close_reason":             nil,
519 | 			},
520 | 		},
521 | 	}
522 | 
523 | 	errorResp := map[string]interface{}{
524 | 		"error": map[string]interface{}{
525 | 			"code":        "BAD_REQUEST_ERROR",
526 | 			"description": "The id provided is not a valid id",
527 | 		},
528 | 	}
529 | 
530 | 	tests := []RazorpayToolTestCase{
531 | 		{
532 | 			Name: "successful fetch QR codes by payment ID",
533 | 			Request: map[string]interface{}{
534 | 				"payment_id": "pay_Di5iqCqA1WEHq6",
535 | 			},
536 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
537 | 				return mock.NewHTTPClient(
538 | 					mock.Endpoint{
539 | 						Path:     qrCodesPath,
540 | 						Method:   "GET",
541 | 						Response: paymentQRCodesResp,
542 | 					},
543 | 				)
544 | 			},
545 | 			ExpectError:    false,
546 | 			ExpectedResult: paymentQRCodesResp,
547 | 		},
548 | 		{
549 | 			Name:           "missing required payment_id parameter",
550 | 			Request:        map[string]interface{}{},
551 | 			MockHttpClient: nil,
552 | 			ExpectError:    true,
553 | 			ExpectedErrMsg: "missing required parameter: payment_id",
554 | 		},
555 | 		{
556 | 			Name: "validator error - invalid payment_id parameter type",
557 | 			Request: map[string]interface{}{
558 | 				"payment_id": 12345,
559 | 			},
560 | 			MockHttpClient: nil,
561 | 			ExpectError:    true,
562 | 			ExpectedErrMsg: "invalid parameter type: payment_id",
563 | 		},
564 | 		{
565 | 			Name: "API error - invalid payment ID",
566 | 			Request: map[string]interface{}{
567 | 				"payment_id": "invalid_payment_id",
568 | 			},
569 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
570 | 				return mock.NewHTTPClient(
571 | 					mock.Endpoint{
572 | 						Path:     qrCodesPath,
573 | 						Method:   "GET",
574 | 						Response: errorResp,
575 | 					},
576 | 				)
577 | 			},
578 | 			ExpectError: true,
579 | 			ExpectedErrMsg: "fetching QR codes failed: " +
580 | 				"The id provided is not a valid id",
581 | 		},
582 | 	}
583 | 
584 | 	for _, tc := range tests {
585 | 		t.Run(tc.Name, func(t *testing.T) {
586 | 			runToolTest(t, tc, FetchQRCodesByPaymentID, "QR Codes by Payment ID")
587 | 		})
588 | 	}
589 | }
590 | 
591 | func TestFetchQRCode(t *testing.T) {
592 | 	// Initialize necessary variables
593 | 	qrID := "qr_FuZIYx6rMbP6gs"
594 | 	apiPath := fmt.Sprintf(
595 | 		"/%s%s/%s",
596 | 		constants.VERSION_V1,
597 | 		constants.QRCODE_URL,
598 | 		qrID,
599 | 	)
600 | 
601 | 	// Successful response based on Razorpay docs
602 | 	successResponse := map[string]interface{}{
603 | 		"id":                       qrID,
604 | 		"entity":                   "qr_code",
605 | 		"created_at":               float64(1623915088),
606 | 		"name":                     "Store_1",
607 | 		"usage":                    "single_use",
608 | 		"type":                     "upi_qr",
609 | 		"image_url":                "https://rzp.io/i/oCswTOcCo",
610 | 		"payment_amount":           float64(300),
611 | 		"status":                   "active",
612 | 		"description":              "For Store 1",
613 | 		"fixed_amount":             true,
614 | 		"payments_amount_received": float64(0),
615 | 		"payments_count_received":  float64(0),
616 | 		"notes": map[string]interface{}{
617 | 			"purpose": "Test UPI QR Code notes",
618 | 		},
619 | 		"customer_id":  "cust_HKsR5se84c5LTO",
620 | 		"close_by":     float64(1681615838),
621 | 		"closed_at":    nil,
622 | 		"close_reason": nil,
623 | 	}
624 | 
625 | 	errorResp := map[string]interface{}{
626 | 		"error": map[string]interface{}{
627 | 			"code":        "BAD_REQUEST_ERROR",
628 | 			"description": "The QR code ID provided is invalid",
629 | 		},
630 | 	}
631 | 
632 | 	tests := []RazorpayToolTestCase{
633 | 		{
634 | 			Name: "successful fetch QR code by ID",
635 | 			Request: map[string]interface{}{
636 | 				"qr_code_id": qrID,
637 | 			},
638 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
639 | 				return mock.NewHTTPClient(
640 | 					mock.Endpoint{
641 | 						Path:     apiPath,
642 | 						Method:   "GET",
643 | 						Response: successResponse,
644 | 					},
645 | 				)
646 | 			},
647 | 			ExpectError:    false,
648 | 			ExpectedResult: successResponse,
649 | 		},
650 | 		{
651 | 			Name:           "missing required qr_code_id parameter",
652 | 			Request:        map[string]interface{}{},
653 | 			MockHttpClient: nil,
654 | 			ExpectError:    true,
655 | 			ExpectedErrMsg: "missing required parameter: qr_code_id",
656 | 		},
657 | 		{
658 | 			Name: "validator error - invalid qr_code_id parameter type",
659 | 			Request: map[string]interface{}{
660 | 				"qr_code_id": 12345,
661 | 			},
662 | 			MockHttpClient: nil,
663 | 			ExpectError:    true,
664 | 			ExpectedErrMsg: "invalid parameter type: qr_code_id",
665 | 		},
666 | 		{
667 | 			Name: "API error - invalid QR code ID",
668 | 			Request: map[string]interface{}{
669 | 				"qr_code_id": qrID,
670 | 			},
671 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
672 | 				return mock.NewHTTPClient(
673 | 					mock.Endpoint{
674 | 						Path:     apiPath,
675 | 						Method:   "GET",
676 | 						Response: errorResp,
677 | 					},
678 | 				)
679 | 			},
680 | 			ExpectError: true,
681 | 			ExpectedErrMsg: "fetching QR code failed: " +
682 | 				"The QR code ID provided is invalid",
683 | 		},
684 | 	}
685 | 
686 | 	for _, tc := range tests {
687 | 		t.Run(tc.Name, func(t *testing.T) {
688 | 			runToolTest(t, tc, FetchQRCode, "QR Code")
689 | 		})
690 | 	}
691 | }
692 | 
693 | func TestFetchPaymentsForQRCode(t *testing.T) {
694 | 	apiPath := "/" + constants.VERSION_V1 +
695 | 		constants.QRCODE_URL + "/qr_test123/payments"
696 | 
697 | 	successResponse := map[string]interface{}{
698 | 		"entity": "collection",
699 | 		"count":  float64(2),
700 | 		"items": []interface{}{
701 | 			map[string]interface{}{
702 | 				"id":              "pay_test123",
703 | 				"entity":          "payment",
704 | 				"amount":          float64(500),
705 | 				"currency":        "INR",
706 | 				"status":          "captured",
707 | 				"method":          "upi",
708 | 				"amount_refunded": float64(0),
709 | 				"refund_status":   nil,
710 | 				"captured":        true,
711 | 				"description":     "QRv2 Payment",
712 | 				"customer_id":     "cust_test123",
713 | 				"created_at":      float64(1623662800),
714 | 			},
715 | 			map[string]interface{}{
716 | 				"id":              "pay_test456",
717 | 				"entity":          "payment",
718 | 				"amount":          float64(1000),
719 | 				"currency":        "INR",
720 | 				"status":          "refunded",
721 | 				"method":          "upi",
722 | 				"amount_refunded": float64(1000),
723 | 				"refund_status":   "full",
724 | 				"captured":        true,
725 | 				"description":     "QRv2 Payment",
726 | 				"customer_id":     "cust_test123",
727 | 				"created_at":      float64(1623661533),
728 | 			},
729 | 		},
730 | 	}
731 | 
732 | 	tests := []RazorpayToolTestCase{
733 | 		{
734 | 			Name: "successful fetch payments for QR code",
735 | 			Request: map[string]interface{}{
736 | 				"qr_code_id": "qr_test123",
737 | 				"count":      10,
738 | 				"from":       1623661000,
739 | 				"to":         1623663000,
740 | 				"skip":       0,
741 | 			},
742 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
743 | 				return mock.NewHTTPClient(
744 | 					mock.Endpoint{
745 | 						Path:     apiPath,
746 | 						Method:   "GET",
747 | 						Response: successResponse,
748 | 					},
749 | 				)
750 | 			},
751 | 			ExpectError:    false,
752 | 			ExpectedResult: successResponse,
753 | 		},
754 | 		{
755 | 			Name: "missing required parameter",
756 | 			Request: map[string]interface{}{
757 | 				"count": 10,
758 | 			},
759 | 			MockHttpClient: nil,
760 | 			ExpectError:    true,
761 | 			ExpectedErrMsg: "missing required parameter: qr_code_id",
762 | 		},
763 | 		{
764 | 			Name: "invalid parameter type",
765 | 			Request: map[string]interface{}{
766 | 				"qr_code_id": 123,
767 | 			},
768 | 			MockHttpClient: nil,
769 | 			ExpectError:    true,
770 | 			ExpectedErrMsg: "invalid parameter type: qr_code_id",
771 | 		},
772 | 		{
773 | 			Name: "API error",
774 | 			Request: map[string]interface{}{
775 | 				"qr_code_id": "qr_test123",
776 | 			},
777 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
778 | 				return mock.NewHTTPClient(
779 | 					mock.Endpoint{
780 | 						Path:   apiPath,
781 | 						Method: "GET",
782 | 						Response: map[string]interface{}{
783 | 							"error": map[string]interface{}{
784 | 								"code":        "BAD_REQUEST_ERROR",
785 | 								"description": "mock error",
786 | 							},
787 | 						},
788 | 					},
789 | 				)
790 | 			},
791 | 			ExpectError:    true,
792 | 			ExpectedErrMsg: "fetching payments for QR code failed: mock error",
793 | 		},
794 | 	}
795 | 
796 | 	for _, tc := range tests {
797 | 		t.Run(tc.Name, func(t *testing.T) {
798 | 			runToolTest(t, tc, FetchPaymentsForQRCode, "QR Code Payments")
799 | 		})
800 | 	}
801 | }
802 | 
803 | func TestCloseQRCode(t *testing.T) {
804 | 	successResponse := map[string]interface{}{
805 | 		"id":                       "qr_HMsVL8HOpbMcjU",
806 | 		"entity":                   "qr_code",
807 | 		"created_at":               float64(1623660301),
808 | 		"name":                     "Store_1",
809 | 		"usage":                    "single_use",
810 | 		"type":                     "upi_qr",
811 | 		"image_url":                "https://rzp.io/i/BWcUVrLp",
812 | 		"payment_amount":           float64(300),
813 | 		"status":                   "closed",
814 | 		"description":              "For Store 1",
815 | 		"fixed_amount":             true,
816 | 		"payments_amount_received": float64(0),
817 | 		"payments_count_received":  float64(0),
818 | 		"notes": map[string]interface{}{
819 | 			"purpose": "Test UPI QR Code notes",
820 | 		},
821 | 		"customer_id":  "cust_HKsR5se84c5LTO",
822 | 		"close_by":     float64(1681615838),
823 | 		"closed_at":    float64(1623660445),
824 | 		"close_reason": "on_demand",
825 | 	}
826 | 
827 | 	baseAPIPath := fmt.Sprintf("/%s%s", constants.VERSION_V1, constants.QRCODE_URL)
828 | 	qrCodeID := "qr_HMsVL8HOpbMcjU"
829 | 	apiPath := fmt.Sprintf("%s/%s/close", baseAPIPath, qrCodeID)
830 | 
831 | 	tests := []RazorpayToolTestCase{
832 | 		{
833 | 			Name: "successful close QR code",
834 | 			Request: map[string]interface{}{
835 | 				"qr_code_id": qrCodeID,
836 | 			},
837 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
838 | 				return mock.NewHTTPClient(
839 | 					mock.Endpoint{
840 | 						Path:     apiPath,
841 | 						Method:   "POST",
842 | 						Response: successResponse,
843 | 					},
844 | 				)
845 | 			},
846 | 			ExpectError:    false,
847 | 			ExpectedResult: successResponse,
848 | 		},
849 | 		{
850 | 			Name:           "missing required qr_code_id parameter",
851 | 			Request:        map[string]interface{}{},
852 | 			MockHttpClient: nil,
853 | 			ExpectError:    true,
854 | 			ExpectedErrMsg: "missing required parameter: qr_code_id",
855 | 		},
856 | 	}
857 | 
858 | 	for _, tc := range tests {
859 | 		t.Run(tc.Name, func(t *testing.T) {
860 | 			runToolTest(t, tc, CloseQRCode, "QR Code")
861 | 		})
862 | 	}
863 | }
864 | 
```
Page 3/5FirstPrevNextLast