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