#
tokens: 42623/50000 8/59 files (page 2/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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/settlements.go:
--------------------------------------------------------------------------------

```go
  1 | package razorpay
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 
  7 | 	rzpsdk "github.com/razorpay/razorpay-go"
  8 | 
  9 | 	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
 10 | 	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
 11 | )
 12 | 
 13 | // FetchSettlement returns a tool that fetches a settlement by ID
 14 | func FetchSettlement(
 15 | 	obs *observability.Observability,
 16 | 	client *rzpsdk.Client,
 17 | ) mcpgo.Tool {
 18 | 	parameters := []mcpgo.ToolParameter{
 19 | 		mcpgo.WithString(
 20 | 			"settlement_id",
 21 | 			mcpgo.Description("The ID of the settlement to fetch."+
 22 | 				"ID starts with the 'setl_'"),
 23 | 			mcpgo.Required(),
 24 | 		),
 25 | 	}
 26 | 
 27 | 	handler := func(
 28 | 		ctx context.Context,
 29 | 		r mcpgo.CallToolRequest,
 30 | 	) (*mcpgo.ToolResult, error) {
 31 | 		client, err := getClientFromContextOrDefault(ctx, client)
 32 | 		if err != nil {
 33 | 			return mcpgo.NewToolResultError(err.Error()), nil
 34 | 		}
 35 | 
 36 | 		// Create a parameters map to collect validated parameters
 37 | 		fetchSettlementOptions := make(map[string]interface{})
 38 | 
 39 | 		// Validate using fluent validator
 40 | 		validator := NewValidator(&r).
 41 | 			ValidateAndAddRequiredString(fetchSettlementOptions, "settlement_id")
 42 | 
 43 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
 44 | 			return result, err
 45 | 		}
 46 | 
 47 | 		settlementID := fetchSettlementOptions["settlement_id"].(string)
 48 | 		settlement, err := client.Settlement.Fetch(settlementID, nil, nil)
 49 | 		if err != nil {
 50 | 			return mcpgo.NewToolResultError(
 51 | 				fmt.Sprintf("fetching settlement failed: %s", err.Error())), nil
 52 | 		}
 53 | 
 54 | 		return mcpgo.NewToolResultJSON(settlement)
 55 | 	}
 56 | 
 57 | 	return mcpgo.NewTool(
 58 | 		"fetch_settlement_with_id",
 59 | 		"Fetch details of a specific settlement using its ID",
 60 | 		parameters,
 61 | 		handler,
 62 | 	)
 63 | }
 64 | 
 65 | // FetchSettlementRecon returns a tool that fetches settlement
 66 | // reconciliation reports
 67 | func FetchSettlementRecon(
 68 | 	obs *observability.Observability,
 69 | 	client *rzpsdk.Client,
 70 | ) mcpgo.Tool {
 71 | 	parameters := []mcpgo.ToolParameter{
 72 | 		mcpgo.WithNumber(
 73 | 			"year",
 74 | 			mcpgo.Description("Year for which the settlement report is "+
 75 | 				"requested (YYYY format)"),
 76 | 			mcpgo.Required(),
 77 | 		),
 78 | 		mcpgo.WithNumber(
 79 | 			"month",
 80 | 			mcpgo.Description("Month for which the settlement report is "+
 81 | 				"requested (MM format)"),
 82 | 			mcpgo.Required(),
 83 | 		),
 84 | 		mcpgo.WithNumber(
 85 | 			"day",
 86 | 			mcpgo.Description("Optional: Day for which the settlement report is "+
 87 | 				"requested (DD format)"),
 88 | 		),
 89 | 		mcpgo.WithNumber(
 90 | 			"count",
 91 | 			mcpgo.Description("Optional: Number of records to fetch "+
 92 | 				"(default: 10, max: 100)"),
 93 | 		),
 94 | 		mcpgo.WithNumber(
 95 | 			"skip",
 96 | 			mcpgo.Description("Optional: Number of records to skip for pagination"),
 97 | 		),
 98 | 	}
 99 | 
100 | 	handler := func(
101 | 		ctx context.Context,
102 | 		r mcpgo.CallToolRequest,
103 | 	) (*mcpgo.ToolResult, error) {
104 | 		client, err := getClientFromContextOrDefault(ctx, client)
105 | 		if err != nil {
106 | 			return mcpgo.NewToolResultError(err.Error()), nil
107 | 		}
108 | 
109 | 		// Create a parameters map to collect validated parameters
110 | 		fetchReconOptions := make(map[string]interface{})
111 | 
112 | 		// Validate using fluent validator
113 | 		validator := NewValidator(&r).
114 | 			ValidateAndAddRequiredInt(fetchReconOptions, "year").
115 | 			ValidateAndAddRequiredInt(fetchReconOptions, "month").
116 | 			ValidateAndAddOptionalInt(fetchReconOptions, "day").
117 | 			ValidateAndAddPagination(fetchReconOptions)
118 | 
119 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
120 | 			return result, err
121 | 		}
122 | 
123 | 		report, err := client.Settlement.Reports(fetchReconOptions, nil)
124 | 		if err != nil {
125 | 			return mcpgo.NewToolResultError(
126 | 				fmt.Sprintf("fetching settlement reconciliation report failed: %s",
127 | 					err.Error())), nil
128 | 		}
129 | 
130 | 		return mcpgo.NewToolResultJSON(report)
131 | 	}
132 | 
133 | 	return mcpgo.NewTool(
134 | 		"fetch_settlement_recon_details",
135 | 		"Fetch settlement reconciliation report for a specific time period",
136 | 		parameters,
137 | 		handler,
138 | 	)
139 | }
140 | 
141 | // FetchAllSettlements returns a tool to fetch multiple settlements with
142 | // filtering and pagination
143 | func FetchAllSettlements(
144 | 	obs *observability.Observability,
145 | 	client *rzpsdk.Client,
146 | ) mcpgo.Tool {
147 | 	parameters := []mcpgo.ToolParameter{
148 | 		// Pagination parameters
149 | 		mcpgo.WithNumber(
150 | 			"count",
151 | 			mcpgo.Description("Number of settlement records to fetch "+
152 | 				"(default: 10, max: 100)"),
153 | 			mcpgo.Min(1),
154 | 			mcpgo.Max(100),
155 | 		),
156 | 		mcpgo.WithNumber(
157 | 			"skip",
158 | 			mcpgo.Description("Number of settlement records to skip (default: 0)"),
159 | 			mcpgo.Min(0),
160 | 		),
161 | 		// Time range filters
162 | 		mcpgo.WithNumber(
163 | 			"from",
164 | 			mcpgo.Description("Unix timestamp (in seconds) from when "+
165 | 				"settlements are to be fetched"),
166 | 			mcpgo.Min(0),
167 | 		),
168 | 		mcpgo.WithNumber(
169 | 			"to",
170 | 			mcpgo.Description("Unix timestamp (in seconds) up till when "+
171 | 				"settlements are to be fetched"),
172 | 			mcpgo.Min(0),
173 | 		),
174 | 	}
175 | 
176 | 	handler := func(
177 | 		ctx context.Context,
178 | 		r mcpgo.CallToolRequest,
179 | 	) (*mcpgo.ToolResult, error) {
180 | 		client, err := getClientFromContextOrDefault(ctx, client)
181 | 		if err != nil {
182 | 			return mcpgo.NewToolResultError(err.Error()), nil
183 | 		}
184 | 
185 | 		// Create parameters map to collect validated parameters
186 | 		fetchAllSettlementsOptions := make(map[string]interface{})
187 | 
188 | 		// Validate using fluent validator
189 | 		validator := NewValidator(&r).
190 | 			ValidateAndAddPagination(fetchAllSettlementsOptions).
191 | 			ValidateAndAddOptionalInt(fetchAllSettlementsOptions, "from").
192 | 			ValidateAndAddOptionalInt(fetchAllSettlementsOptions, "to")
193 | 
194 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
195 | 			return result, err
196 | 		}
197 | 
198 | 		// Fetch all settlements using Razorpay SDK
199 | 		settlements, err := client.Settlement.All(fetchAllSettlementsOptions, nil)
200 | 		if err != nil {
201 | 			return mcpgo.NewToolResultError(
202 | 				fmt.Sprintf("fetching settlements failed: %s", err.Error())), nil
203 | 		}
204 | 
205 | 		return mcpgo.NewToolResultJSON(settlements)
206 | 	}
207 | 
208 | 	return mcpgo.NewTool(
209 | 		"fetch_all_settlements",
210 | 		"Fetch all settlements with optional filtering and pagination",
211 | 		parameters,
212 | 		handler,
213 | 	)
214 | }
215 | 
216 | // CreateInstantSettlement returns a tool that creates an instant settlement
217 | func CreateInstantSettlement(
218 | 	obs *observability.Observability,
219 | 	client *rzpsdk.Client,
220 | ) mcpgo.Tool {
221 | 	parameters := []mcpgo.ToolParameter{
222 | 		mcpgo.WithNumber(
223 | 			"amount",
224 | 			mcpgo.Description("The amount you want to get settled instantly in amount in the smallest "+ //nolint:lll
225 | 				"currency sub-unit (e.g., for ₹295, use 29500)"),
226 | 			mcpgo.Required(),
227 | 			mcpgo.Min(200), // Minimum amount is 200 (₹2)
228 | 		),
229 | 		mcpgo.WithBoolean(
230 | 			"settle_full_balance",
231 | 			mcpgo.Description("If true, Razorpay will settle the maximum amount "+
232 | 				"possible and ignore amount parameter"),
233 | 			mcpgo.DefaultValue(false),
234 | 		),
235 | 		mcpgo.WithString(
236 | 			"description",
237 | 			mcpgo.Description("Custom note for the instant settlement."),
238 | 			mcpgo.Max(30),
239 | 			mcpgo.Pattern("^[a-zA-Z0-9 ]*$"),
240 | 		),
241 | 		mcpgo.WithObject(
242 | 			"notes",
243 | 			mcpgo.Description("Key-value pairs for additional information. "+
244 | 				"Max 15 pairs, 256 chars each"),
245 | 			mcpgo.MaxProperties(15),
246 | 		),
247 | 	}
248 | 
249 | 	handler := func(
250 | 		ctx context.Context,
251 | 		r mcpgo.CallToolRequest,
252 | 	) (*mcpgo.ToolResult, error) {
253 | 		client, err := getClientFromContextOrDefault(ctx, client)
254 | 		if err != nil {
255 | 			return mcpgo.NewToolResultError(err.Error()), nil
256 | 		}
257 | 
258 | 		// Create parameters map to collect validated parameters
259 | 		createInstantSettlementReq := make(map[string]interface{})
260 | 
261 | 		// Validate using fluent validator
262 | 		validator := NewValidator(&r).
263 | 			ValidateAndAddRequiredInt(createInstantSettlementReq, "amount").
264 | 			ValidateAndAddOptionalBool(createInstantSettlementReq, "settle_full_balance"). // nolint:lll
265 | 			ValidateAndAddOptionalString(createInstantSettlementReq, "description").
266 | 			ValidateAndAddOptionalMap(createInstantSettlementReq, "notes")
267 | 
268 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
269 | 			return result, err
270 | 		}
271 | 
272 | 		// Create the instant settlement
273 | 		settlement, err := client.Settlement.CreateOnDemandSettlement(
274 | 			createInstantSettlementReq, nil)
275 | 		if err != nil {
276 | 			return mcpgo.NewToolResultError(
277 | 				fmt.Sprintf("creating instant settlement failed: %s",
278 | 					err.Error())), nil
279 | 		}
280 | 
281 | 		return mcpgo.NewToolResultJSON(settlement)
282 | 	}
283 | 
284 | 	return mcpgo.NewTool(
285 | 		"create_instant_settlement",
286 | 		"Create an instant settlement to get funds transferred to your bank account", // nolint:lll
287 | 		parameters,
288 | 		handler,
289 | 	)
290 | }
291 | 
292 | // FetchAllInstantSettlements returns a tool to fetch all instant settlements
293 | // with filtering and pagination
294 | func FetchAllInstantSettlements(
295 | 	obs *observability.Observability,
296 | 	client *rzpsdk.Client,
297 | ) mcpgo.Tool {
298 | 	parameters := []mcpgo.ToolParameter{
299 | 		// Pagination parameters
300 | 		mcpgo.WithNumber(
301 | 			"count",
302 | 			mcpgo.Description("Number of instant settlement records to fetch "+
303 | 				"(default: 10, max: 100)"),
304 | 			mcpgo.Min(1),
305 | 			mcpgo.Max(100),
306 | 		),
307 | 		mcpgo.WithNumber(
308 | 			"skip",
309 | 			mcpgo.Description("Number of instant settlement records to skip (default: 0)"), //nolint:lll
310 | 			mcpgo.Min(0),
311 | 		),
312 | 		// Time range filters
313 | 		mcpgo.WithNumber(
314 | 			"from",
315 | 			mcpgo.Description("Unix timestamp (in seconds) from when "+
316 | 				"instant settlements are to be fetched"),
317 | 			mcpgo.Min(0),
318 | 		),
319 | 		mcpgo.WithNumber(
320 | 			"to",
321 | 			mcpgo.Description("Unix timestamp (in seconds) up till when "+
322 | 				"instant settlements are to be fetched"),
323 | 			mcpgo.Min(0),
324 | 		),
325 | 		// Expand parameter for payout details
326 | 		mcpgo.WithArray(
327 | 			"expand",
328 | 			mcpgo.Description("Pass this if you want to fetch payout details "+
329 | 				"as part of the response for all instant settlements. "+
330 | 				"Supported values: ondemand_payouts"),
331 | 		),
332 | 	}
333 | 
334 | 	handler := func(
335 | 		ctx context.Context,
336 | 		r mcpgo.CallToolRequest,
337 | 	) (*mcpgo.ToolResult, error) {
338 | 		client, err := getClientFromContextOrDefault(ctx, client)
339 | 		if err != nil {
340 | 			return mcpgo.NewToolResultError(err.Error()), nil
341 | 		}
342 | 
343 | 		// Create parameters map to collect validated parameters
344 | 		options := make(map[string]interface{})
345 | 
346 | 		// Validate using fluent validator
347 | 		validator := NewValidator(&r).
348 | 			ValidateAndAddPagination(options).
349 | 			ValidateAndAddExpand(options).
350 | 			ValidateAndAddOptionalInt(options, "from").
351 | 			ValidateAndAddOptionalInt(options, "to")
352 | 
353 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
354 | 			return result, err
355 | 		}
356 | 
357 | 		// Fetch all instant settlements using Razorpay SDK
358 | 		settlements, err := client.Settlement.FetchAllOnDemandSettlement(options, nil)
359 | 		if err != nil {
360 | 			return mcpgo.NewToolResultError(
361 | 				fmt.Sprintf("fetching instant settlements failed: %s", err.Error())), nil
362 | 		}
363 | 
364 | 		return mcpgo.NewToolResultJSON(settlements)
365 | 	}
366 | 
367 | 	return mcpgo.NewTool(
368 | 		"fetch_all_instant_settlements",
369 | 		"Fetch all instant settlements with optional filtering, pagination, and payout details", //nolint:lll
370 | 		parameters,
371 | 		handler,
372 | 	)
373 | }
374 | 
375 | // FetchInstantSettlement returns a tool that fetches instant settlement by ID
376 | func FetchInstantSettlement(
377 | 	obs *observability.Observability,
378 | 	client *rzpsdk.Client,
379 | ) mcpgo.Tool {
380 | 	parameters := []mcpgo.ToolParameter{
381 | 		mcpgo.WithString(
382 | 			"settlement_id",
383 | 			mcpgo.Description("The ID of the instant settlement to fetch. "+
384 | 				"ID starts with 'setlod_'"),
385 | 			mcpgo.Required(),
386 | 		),
387 | 	}
388 | 
389 | 	handler := func(
390 | 		ctx context.Context,
391 | 		r mcpgo.CallToolRequest,
392 | 	) (*mcpgo.ToolResult, error) {
393 | 		client, err := getClientFromContextOrDefault(ctx, client)
394 | 		if err != nil {
395 | 			return mcpgo.NewToolResultError(err.Error()), nil
396 | 		}
397 | 
398 | 		// Create parameters map to collect validated parameters
399 | 		params := make(map[string]interface{})
400 | 
401 | 		// Validate using fluent validator
402 | 		validator := NewValidator(&r).
403 | 			ValidateAndAddRequiredString(params, "settlement_id")
404 | 
405 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
406 | 			return result, err
407 | 		}
408 | 
409 | 		settlementID := params["settlement_id"].(string)
410 | 
411 | 		// Fetch the instant settlement by ID using SDK
412 | 		settlement, err := client.Settlement.FetchOnDemandSettlementById(
413 | 			settlementID, nil, nil)
414 | 		if err != nil {
415 | 			return mcpgo.NewToolResultError(
416 | 				fmt.Sprintf("fetching instant settlement failed: %s", err.Error())), nil
417 | 		}
418 | 
419 | 		return mcpgo.NewToolResultJSON(settlement)
420 | 	}
421 | 
422 | 	return mcpgo.NewTool(
423 | 		"fetch_instant_settlement_with_id",
424 | 		"Fetch details of a specific instant settlement using its ID",
425 | 		parameters,
426 | 		handler,
427 | 	)
428 | }
429 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/orders.go:
--------------------------------------------------------------------------------

```go
  1 | package razorpay
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 
  7 | 	rzpsdk "github.com/razorpay/razorpay-go"
  8 | 
  9 | 	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
 10 | 	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
 11 | )
 12 | 
 13 | // CreateOrder returns a tool that creates new orders in Razorpay
 14 | func CreateOrder(
 15 | 	obs *observability.Observability,
 16 | 	client *rzpsdk.Client,
 17 | ) mcpgo.Tool {
 18 | 	parameters := []mcpgo.ToolParameter{
 19 | 		mcpgo.WithNumber(
 20 | 			"amount",
 21 | 			mcpgo.Description("Payment amount in the smallest "+
 22 | 				"currency sub-unit (e.g., for ₹295, use 29500)"),
 23 | 			mcpgo.Required(),
 24 | 			mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
 25 | 		),
 26 | 		mcpgo.WithString(
 27 | 			"currency",
 28 | 			mcpgo.Description("ISO code for the currency "+
 29 | 				"(e.g., INR, USD, SGD)"),
 30 | 			mcpgo.Required(),
 31 | 			mcpgo.Pattern("^[A-Z]{3}$"), // ISO currency codes are 3 uppercase letters
 32 | 		),
 33 | 		mcpgo.WithString(
 34 | 			"receipt",
 35 | 			mcpgo.Description("Receipt number for internal "+
 36 | 				"reference (max 40 chars, must be unique)"),
 37 | 			mcpgo.Max(40),
 38 | 		),
 39 | 		mcpgo.WithObject(
 40 | 			"notes",
 41 | 			mcpgo.Description("Key-value pairs for additional "+
 42 | 				"information (max 15 pairs, 256 chars each)"),
 43 | 			mcpgo.MaxProperties(15),
 44 | 		),
 45 | 		mcpgo.WithBoolean(
 46 | 			"partial_payment",
 47 | 			mcpgo.Description("Whether the customer can make partial payments"),
 48 | 			mcpgo.DefaultValue(false),
 49 | 		),
 50 | 		mcpgo.WithNumber(
 51 | 			"first_payment_min_amount",
 52 | 			mcpgo.Description("Minimum amount for first partial "+
 53 | 				"payment (only if partial_payment is true)"),
 54 | 			mcpgo.Min(100),
 55 | 		),
 56 | 		mcpgo.WithArray(
 57 | 			"transfers",
 58 | 			mcpgo.Description("Array of transfer objects for distributing "+
 59 | 				"payment amounts among multiple linked accounts. Each transfer "+
 60 | 				"object should contain: account (linked account ID), amount "+
 61 | 				"(in currency subunits), currency (ISO code), and optional fields "+
 62 | 				"like notes, linked_account_notes, on_hold, on_hold_until"),
 63 | 		),
 64 | 		mcpgo.WithString(
 65 | 			"method",
 66 | 			mcpgo.Description("Payment method for mandate orders. "+
 67 | 				"REQUIRED for mandate orders. Must be 'upi' when using "+
 68 | 				"token.type='single_block_multiple_debit'. This field is used "+
 69 | 				"only for mandate/recurring payment orders."),
 70 | 		),
 71 | 		mcpgo.WithString(
 72 | 			"customer_id",
 73 | 			mcpgo.Description("Customer ID for mandate orders. "+
 74 | 				"REQUIRED for mandate orders. Must start with 'cust_' followed by "+
 75 | 				"alphanumeric characters. Example: 'cust_xxx'. "+
 76 | 				"This identifies the customer for recurring payments."),
 77 | 		),
 78 | 		mcpgo.WithObject(
 79 | 			"token",
 80 | 			mcpgo.Description("Token object for mandate orders. "+
 81 | 				"REQUIRED for mandate orders. Must contain: max_amount "+
 82 | 				"(positive number, maximum debit amount), frequency "+
 83 | 				"(as_presented/monthly/one_time/yearly/weekly/daily), "+
 84 | 				"type='single_block_multiple_debit' (only supported type), "+
 85 | 				"and optionally expire_at (Unix timestamp, defaults to today+60days). "+
 86 | 				"Example: {\"max_amount\": 100, \"frequency\": \"as_presented\", "+
 87 | 				"\"type\": \"single_block_multiple_debit\"}"),
 88 | 		),
 89 | 	}
 90 | 
 91 | 	handler := func(
 92 | 		ctx context.Context,
 93 | 		r mcpgo.CallToolRequest,
 94 | 	) (*mcpgo.ToolResult, error) {
 95 | 		// Get client from context or use default
 96 | 		client, err := getClientFromContextOrDefault(ctx, client)
 97 | 		if err != nil {
 98 | 			return mcpgo.NewToolResultError(err.Error()), nil
 99 | 		}
100 | 
101 | 		payload := make(map[string]interface{})
102 | 
103 | 		validator := NewValidator(&r).
104 | 			ValidateAndAddRequiredFloat(payload, "amount").
105 | 			ValidateAndAddRequiredString(payload, "currency").
106 | 			ValidateAndAddOptionalString(payload, "receipt").
107 | 			ValidateAndAddOptionalMap(payload, "notes").
108 | 			ValidateAndAddOptionalBool(payload, "partial_payment").
109 | 			ValidateAndAddOptionalArray(payload, "transfers").
110 | 			ValidateAndAddOptionalString(payload, "method").
111 | 			ValidateAndAddOptionalString(payload, "customer_id").
112 | 			ValidateAndAddToken(payload, "token")
113 | 
114 | 		// Add first_payment_min_amount only if partial_payment is true
115 | 		if payload["partial_payment"] == true {
116 | 			validator.ValidateAndAddOptionalFloat(payload, "first_payment_min_amount")
117 | 		}
118 | 
119 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
120 | 			return result, err
121 | 		}
122 | 
123 | 		order, err := client.Order.Create(payload, nil)
124 | 		if err != nil {
125 | 			return mcpgo.NewToolResultError(
126 | 				fmt.Sprintf("creating order failed: %s", err.Error()),
127 | 			), nil
128 | 		}
129 | 
130 | 		return mcpgo.NewToolResultJSON(order)
131 | 	}
132 | 
133 | 	return mcpgo.NewTool(
134 | 		"create_order",
135 | 		"Create a new order in Razorpay. Supports both regular orders and "+
136 | 			"mandate orders. "+
137 | 			"\n\nFor REGULAR ORDERS: Provide amount, currency, and optional "+
138 | 			"receipt/notes. "+
139 | 			"\n\nFor MANDATE ORDERS (recurring payments): You MUST provide ALL "+
140 | 			"of these fields: "+
141 | 			"amount, currency, method='upi', customer_id (starts with 'cust_'), "+
142 | 			"and token object. "+
143 | 			"\n\nThe token object is required for mandate orders and must contain: "+
144 | 			"max_amount (positive number), frequency "+
145 | 			"(as_presented/monthly/one_time/yearly/weekly/daily), "+
146 | 			"type='single_block_multiple_debit', and optionally expire_at "+
147 | 			"(defaults to today+60days). "+
148 | 			"\n\nIMPORTANT: When token.type is 'single_block_multiple_debit', "+
149 | 			"the method MUST be 'upi'. "+
150 | 			"\n\nExample mandate order payload: "+
151 | 			`{"amount": 100, "currency": "INR", "method": "upi", `+
152 | 			`"customer_id": "cust_abc123", `+
153 | 			`"token": {"max_amount": 100, "frequency": "as_presented", `+
154 | 			`"type": "single_block_multiple_debit"}, `+
155 | 			`"receipt": "Receipt No. 1", "notes": {"key": "value"}}`,
156 | 		parameters,
157 | 		handler,
158 | 	)
159 | }
160 | 
161 | // FetchOrder returns a tool to fetch order details by ID
162 | func FetchOrder(
163 | 	obs *observability.Observability,
164 | 	client *rzpsdk.Client,
165 | ) mcpgo.Tool {
166 | 	parameters := []mcpgo.ToolParameter{
167 | 		mcpgo.WithString(
168 | 			"order_id",
169 | 			mcpgo.Description("Unique identifier of the order to be retrieved"),
170 | 			mcpgo.Required(),
171 | 		),
172 | 	}
173 | 
174 | 	handler := func(
175 | 		ctx context.Context,
176 | 		r mcpgo.CallToolRequest,
177 | 	) (*mcpgo.ToolResult, error) {
178 | 		// Get client from context or use default
179 | 		client, err := getClientFromContextOrDefault(ctx, client)
180 | 		if err != nil {
181 | 			return mcpgo.NewToolResultError(err.Error()), nil
182 | 		}
183 | 
184 | 		payload := make(map[string]interface{})
185 | 
186 | 		validator := NewValidator(&r).
187 | 			ValidateAndAddRequiredString(payload, "order_id")
188 | 
189 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
190 | 			return result, err
191 | 		}
192 | 
193 | 		order, err := client.Order.Fetch(payload["order_id"].(string), nil, nil)
194 | 		if err != nil {
195 | 			return mcpgo.NewToolResultError(
196 | 				fmt.Sprintf("fetching order failed: %s", err.Error()),
197 | 			), nil
198 | 		}
199 | 
200 | 		return mcpgo.NewToolResultJSON(order)
201 | 	}
202 | 
203 | 	return mcpgo.NewTool(
204 | 		"fetch_order",
205 | 		"Fetch an order's details using its ID",
206 | 		parameters,
207 | 		handler,
208 | 	)
209 | }
210 | 
211 | // FetchAllOrders returns a tool to fetch all orders with optional filtering
212 | func FetchAllOrders(
213 | 	obs *observability.Observability,
214 | 	client *rzpsdk.Client,
215 | ) mcpgo.Tool {
216 | 	parameters := []mcpgo.ToolParameter{
217 | 		mcpgo.WithNumber(
218 | 			"count",
219 | 			mcpgo.Description("Number of orders to be fetched "+
220 | 				"(default: 10, max: 100)"),
221 | 			mcpgo.Min(1),
222 | 			mcpgo.Max(100),
223 | 		),
224 | 		mcpgo.WithNumber(
225 | 			"skip",
226 | 			mcpgo.Description("Number of orders to be skipped (default: 0)"),
227 | 			mcpgo.Min(0),
228 | 		),
229 | 		mcpgo.WithNumber(
230 | 			"from",
231 | 			mcpgo.Description("Timestamp (in Unix format) from when "+
232 | 				"the orders should be fetched"),
233 | 			mcpgo.Min(0),
234 | 		),
235 | 		mcpgo.WithNumber(
236 | 			"to",
237 | 			mcpgo.Description("Timestamp (in Unix format) up till "+
238 | 				"when orders are to be fetched"),
239 | 			mcpgo.Min(0),
240 | 		),
241 | 		mcpgo.WithNumber(
242 | 			"authorized",
243 | 			mcpgo.Description("Filter orders based on payment authorization status. "+
244 | 				"Values: 0 (orders with unauthorized payments), "+
245 | 				"1 (orders with authorized payments)"),
246 | 			mcpgo.Min(0),
247 | 			mcpgo.Max(1),
248 | 		),
249 | 		mcpgo.WithString(
250 | 			"receipt",
251 | 			mcpgo.Description("Filter orders that contain the "+
252 | 				"provided value for receipt"),
253 | 		),
254 | 		mcpgo.WithArray(
255 | 			"expand",
256 | 			mcpgo.Description("Used to retrieve additional information. "+
257 | 				"Supported values: payments, payments.card, transfers, virtual_account"),
258 | 		),
259 | 	}
260 | 
261 | 	handler := func(
262 | 		ctx context.Context,
263 | 		r mcpgo.CallToolRequest,
264 | 	) (*mcpgo.ToolResult, error) {
265 | 		// Get client from context or use default
266 | 		client, err := getClientFromContextOrDefault(ctx, client)
267 | 		if err != nil {
268 | 			return mcpgo.NewToolResultError(err.Error()), nil
269 | 		}
270 | 
271 | 		queryParams := make(map[string]interface{})
272 | 
273 | 		validator := NewValidator(&r).
274 | 			ValidateAndAddPagination(queryParams).
275 | 			ValidateAndAddOptionalInt(queryParams, "from").
276 | 			ValidateAndAddOptionalInt(queryParams, "to").
277 | 			ValidateAndAddOptionalInt(queryParams, "authorized").
278 | 			ValidateAndAddOptionalString(queryParams, "receipt").
279 | 			ValidateAndAddExpand(queryParams)
280 | 
281 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
282 | 			return result, err
283 | 		}
284 | 
285 | 		orders, err := client.Order.All(queryParams, nil)
286 | 		if err != nil {
287 | 			return mcpgo.NewToolResultError(
288 | 				fmt.Sprintf("fetching orders failed: %s", err.Error()),
289 | 			), nil
290 | 		}
291 | 
292 | 		return mcpgo.NewToolResultJSON(orders)
293 | 	}
294 | 
295 | 	return mcpgo.NewTool(
296 | 		"fetch_all_orders",
297 | 		"Fetch all orders with optional filtering and pagination",
298 | 		parameters,
299 | 		handler,
300 | 	)
301 | }
302 | 
303 | // FetchOrderPayments returns a tool to fetch all payments for a specific order
304 | func FetchOrderPayments(
305 | 	obs *observability.Observability,
306 | 	client *rzpsdk.Client,
307 | ) mcpgo.Tool {
308 | 	parameters := []mcpgo.ToolParameter{
309 | 		mcpgo.WithString(
310 | 			"order_id",
311 | 			mcpgo.Description(
312 | 				"Unique identifier of the order for which payments should"+
313 | 					" be retrieved. Order id should start with `order_`"),
314 | 			mcpgo.Required(),
315 | 		),
316 | 	}
317 | 
318 | 	handler := func(
319 | 		ctx context.Context,
320 | 		r mcpgo.CallToolRequest,
321 | 	) (*mcpgo.ToolResult, error) {
322 | 		// Get client from context or use default
323 | 		client, err := getClientFromContextOrDefault(ctx, client)
324 | 		if err != nil {
325 | 			return mcpgo.NewToolResultError(err.Error()), nil
326 | 		}
327 | 
328 | 		orderPaymentsReq := make(map[string]interface{})
329 | 
330 | 		validator := NewValidator(&r).
331 | 			ValidateAndAddRequiredString(orderPaymentsReq, "order_id")
332 | 
333 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
334 | 			return result, err
335 | 		}
336 | 
337 | 		// Fetch payments for the order using Razorpay SDK
338 | 		// Note: Using the Order.Payments method from SDK
339 | 		orderID := orderPaymentsReq["order_id"].(string)
340 | 		payments, err := client.Order.Payments(orderID, nil, nil)
341 | 		if err != nil {
342 | 			return mcpgo.NewToolResultError(
343 | 				fmt.Sprintf(
344 | 					"fetching payments for order failed: %s",
345 | 					err.Error(),
346 | 				),
347 | 			), nil
348 | 		}
349 | 
350 | 		// Return the result as JSON
351 | 		return mcpgo.NewToolResultJSON(payments)
352 | 	}
353 | 
354 | 	return mcpgo.NewTool(
355 | 		"fetch_order_payments",
356 | 		"Fetch all payments made for a specific order in Razorpay",
357 | 		parameters,
358 | 		handler,
359 | 	)
360 | }
361 | 
362 | // UpdateOrder returns a tool to update an order
363 | // only the order's notes can be updated
364 | func UpdateOrder(
365 | 	obs *observability.Observability,
366 | 	client *rzpsdk.Client,
367 | ) mcpgo.Tool {
368 | 	parameters := []mcpgo.ToolParameter{
369 | 		mcpgo.WithString(
370 | 			"order_id",
371 | 			mcpgo.Description("Unique identifier of the order which "+
372 | 				"needs to be updated. ID should have an order_ prefix."),
373 | 			mcpgo.Required(),
374 | 		),
375 | 		mcpgo.WithObject(
376 | 			"notes",
377 | 			mcpgo.Description("Key-value pairs used to store additional "+
378 | 				"information about the order. A maximum of 15 key-value pairs "+
379 | 				"can be included, with each value not exceeding 256 characters."),
380 | 			mcpgo.Required(),
381 | 		),
382 | 	}
383 | 
384 | 	handler := func(
385 | 		ctx context.Context,
386 | 		r mcpgo.CallToolRequest,
387 | 	) (*mcpgo.ToolResult, error) {
388 | 		orderUpdateReq := make(map[string]interface{})
389 | 		data := make(map[string]interface{})
390 | 
391 | 		client, err := getClientFromContextOrDefault(ctx, client)
392 | 		if err != nil {
393 | 			return mcpgo.NewToolResultError(err.Error()), nil
394 | 		}
395 | 
396 | 		validator := NewValidator(&r).
397 | 			ValidateAndAddRequiredString(orderUpdateReq, "order_id").
398 | 			ValidateAndAddRequiredMap(orderUpdateReq, "notes")
399 | 
400 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
401 | 			return result, err
402 | 		}
403 | 
404 | 		data["notes"] = orderUpdateReq["notes"]
405 | 		orderID := orderUpdateReq["order_id"].(string)
406 | 
407 | 		order, err := client.Order.Update(orderID, data, nil)
408 | 		if err != nil {
409 | 			return mcpgo.NewToolResultError(
410 | 				fmt.Sprintf("updating order failed: %s", err.Error())), nil
411 | 		}
412 | 
413 | 		return mcpgo.NewToolResultJSON(order)
414 | 	}
415 | 
416 | 	return mcpgo.NewTool(
417 | 		"update_order",
418 | 		"Use this tool to update the notes for a specific order. "+
419 | 			"Only the notes field can be modified.",
420 | 		parameters,
421 | 		handler,
422 | 	)
423 | }
424 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/tools_params.go:
--------------------------------------------------------------------------------

```go
  1 | package razorpay
  2 | 
  3 | import (
  4 | 	"encoding/json"
  5 | 	"errors"
  6 | 	"strings"
  7 | 	"time"
  8 | 
  9 | 	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
 10 | )
 11 | 
 12 | // Validator provides a fluent interface for validating parameters
 13 | // and collecting errors
 14 | type Validator struct {
 15 | 	request *mcpgo.CallToolRequest
 16 | 	errors  []error
 17 | }
 18 | 
 19 | // NewValidator creates a new validator for the given request
 20 | func NewValidator(r *mcpgo.CallToolRequest) *Validator {
 21 | 	return &Validator{
 22 | 		request: r,
 23 | 		errors:  []error{},
 24 | 	}
 25 | }
 26 | 
 27 | // addError adds a non-nil error to the collection
 28 | func (v *Validator) addError(err error) *Validator {
 29 | 	if err != nil {
 30 | 		v.errors = append(v.errors, err)
 31 | 	}
 32 | 	return v
 33 | }
 34 | 
 35 | // HasErrors returns true if there are any validation errors
 36 | func (v *Validator) HasErrors() bool {
 37 | 	return len(v.errors) > 0
 38 | }
 39 | 
 40 | // HandleErrorsIfAny formats all errors and returns an appropriate tool result
 41 | func (v *Validator) HandleErrorsIfAny() (*mcpgo.ToolResult, error) {
 42 | 	if v.HasErrors() {
 43 | 		messages := make([]string, 0, len(v.errors))
 44 | 		for _, err := range v.errors {
 45 | 			messages = append(messages, err.Error())
 46 | 		}
 47 | 		errorMsg := "Validation errors:\n- " + strings.Join(messages, "\n- ")
 48 | 		return mcpgo.NewToolResultError(errorMsg), nil
 49 | 	}
 50 | 	return nil, nil
 51 | }
 52 | 
 53 | // extractValueGeneric is a standalone generic function to extract a parameter
 54 | // of type T
 55 | func extractValueGeneric[T any](
 56 | 	request *mcpgo.CallToolRequest,
 57 | 	name string,
 58 | 	required bool,
 59 | ) (*T, error) {
 60 | 	// Type assert Arguments from any to map[string]interface{}
 61 | 	args, ok := request.Arguments.(map[string]interface{})
 62 | 	if !ok {
 63 | 		return nil, errors.New("invalid arguments type")
 64 | 	}
 65 | 
 66 | 	val, ok := args[name]
 67 | 	if !ok || val == nil {
 68 | 		if required {
 69 | 			return nil, errors.New("missing required parameter: " + name)
 70 | 		}
 71 | 		return nil, nil // Not an error for optional params
 72 | 	}
 73 | 
 74 | 	var result T
 75 | 	data, err := json.Marshal(val)
 76 | 	if err != nil {
 77 | 		return nil, errors.New("invalid parameter type: " + name)
 78 | 	}
 79 | 
 80 | 	err = json.Unmarshal(data, &result)
 81 | 	if err != nil {
 82 | 		return nil, errors.New("invalid parameter type: " + name)
 83 | 	}
 84 | 
 85 | 	return &result, nil
 86 | }
 87 | 
 88 | // Generic validation functions
 89 | 
 90 | // validateAndAddRequired validates and adds a required parameter of any type
 91 | func validateAndAddRequired[T any](
 92 | 	v *Validator,
 93 | 	params map[string]interface{},
 94 | 	name string,
 95 | ) *Validator {
 96 | 	value, err := extractValueGeneric[T](v.request, name, true)
 97 | 	if err != nil {
 98 | 		return v.addError(err)
 99 | 	}
100 | 
101 | 	if value == nil {
102 | 		return v
103 | 	}
104 | 
105 | 	params[name] = *value
106 | 	return v
107 | }
108 | 
109 | // validateAndAddOptional validates and adds an optional parameter of any type
110 | // if not empty
111 | func validateAndAddOptional[T any](
112 | 	v *Validator,
113 | 	params map[string]interface{},
114 | 	name string,
115 | ) *Validator {
116 | 	value, err := extractValueGeneric[T](v.request, name, false)
117 | 	if err != nil {
118 | 		return v.addError(err)
119 | 	}
120 | 
121 | 	if value == nil {
122 | 		return v
123 | 	}
124 | 
125 | 	params[name] = *value
126 | 
127 | 	return v
128 | }
129 | 
130 | // validateAndAddToPath is a generic helper to extract a value and write it into
131 | // `target[targetKey]` if non-empty
132 | func validateAndAddToPath[T any](
133 | 	v *Validator,
134 | 	target map[string]interface{},
135 | 	paramName string,
136 | 	targetKey string,
137 | ) *Validator {
138 | 	value, err := extractValueGeneric[T](v.request, paramName, false)
139 | 	if err != nil {
140 | 		return v.addError(err)
141 | 	}
142 | 
143 | 	if value == nil {
144 | 		return v
145 | 	}
146 | 
147 | 	target[targetKey] = *value
148 | 
149 | 	return v
150 | }
151 | 
152 | // ValidateAndAddOptionalStringToPath validates an optional string
153 | // and writes it into target[targetKey]
154 | func (v *Validator) ValidateAndAddOptionalStringToPath(
155 | 	target map[string]interface{},
156 | 	paramName, targetKey string,
157 | ) *Validator {
158 | 	return validateAndAddToPath[string](v, target, paramName, targetKey) // nolint:lll
159 | }
160 | 
161 | // ValidateAndAddOptionalBoolToPath validates an optional bool
162 | // and writes it into target[targetKey]
163 | // only if it was explicitly provided in the request
164 | func (v *Validator) ValidateAndAddOptionalBoolToPath(
165 | 	target map[string]interface{},
166 | 	paramName, targetKey string,
167 | ) *Validator {
168 | 	// Now validate and add the parameter
169 | 	value, err := extractValueGeneric[bool](v.request, paramName, false)
170 | 	if err != nil {
171 | 		return v.addError(err)
172 | 	}
173 | 
174 | 	if value == nil {
175 | 		return v
176 | 	}
177 | 
178 | 	target[targetKey] = *value
179 | 	return v
180 | }
181 | 
182 | // ValidateAndAddOptionalIntToPath validates an optional integer
183 | // and writes it into target[targetKey]
184 | func (v *Validator) ValidateAndAddOptionalIntToPath(
185 | 	target map[string]interface{},
186 | 	paramName, targetKey string,
187 | ) *Validator {
188 | 	return validateAndAddToPath[int64](v, target, paramName, targetKey)
189 | }
190 | 
191 | // Type-specific validator methods
192 | 
193 | // ValidateAndAddRequiredString validates and adds a required string parameter
194 | func (v *Validator) ValidateAndAddRequiredString(
195 | 	params map[string]interface{},
196 | 	name string,
197 | ) *Validator {
198 | 	return validateAndAddRequired[string](v, params, name)
199 | }
200 | 
201 | // ValidateAndAddOptionalString validates and adds an optional string parameter
202 | func (v *Validator) ValidateAndAddOptionalString(
203 | 	params map[string]interface{},
204 | 	name string,
205 | ) *Validator {
206 | 	return validateAndAddOptional[string](v, params, name)
207 | }
208 | 
209 | // ValidateAndAddRequiredMap validates and adds a required map parameter
210 | func (v *Validator) ValidateAndAddRequiredMap(
211 | 	params map[string]interface{},
212 | 	name string,
213 | ) *Validator {
214 | 	return validateAndAddRequired[map[string]interface{}](v, params, name)
215 | }
216 | 
217 | // ValidateAndAddOptionalMap validates and adds an optional map parameter
218 | func (v *Validator) ValidateAndAddOptionalMap(
219 | 	params map[string]interface{},
220 | 	name string,
221 | ) *Validator {
222 | 	return validateAndAddOptional[map[string]interface{}](v, params, name)
223 | }
224 | 
225 | // ValidateAndAddRequiredArray validates and adds a required array parameter
226 | func (v *Validator) ValidateAndAddRequiredArray(
227 | 	params map[string]interface{},
228 | 	name string,
229 | ) *Validator {
230 | 	return validateAndAddRequired[[]interface{}](v, params, name)
231 | }
232 | 
233 | // ValidateAndAddOptionalArray validates and adds an optional array parameter
234 | func (v *Validator) ValidateAndAddOptionalArray(
235 | 	params map[string]interface{},
236 | 	name string,
237 | ) *Validator {
238 | 	return validateAndAddOptional[[]interface{}](v, params, name)
239 | }
240 | 
241 | // ValidateAndAddPagination validates and adds pagination parameters
242 | // (count and skip)
243 | func (v *Validator) ValidateAndAddPagination(
244 | 	params map[string]interface{},
245 | ) *Validator {
246 | 	return v.ValidateAndAddOptionalInt(params, "count").
247 | 		ValidateAndAddOptionalInt(params, "skip")
248 | }
249 | 
250 | // ValidateAndAddExpand validates and adds expand parameters
251 | func (v *Validator) ValidateAndAddExpand(
252 | 	params map[string]interface{},
253 | ) *Validator {
254 | 	expand, err := extractValueGeneric[[]string](v.request, "expand", false)
255 | 	if err != nil {
256 | 		return v.addError(err)
257 | 	}
258 | 
259 | 	if expand == nil {
260 | 		return v
261 | 	}
262 | 
263 | 	if len(*expand) > 0 {
264 | 		for _, val := range *expand {
265 | 			params["expand[]"] = val
266 | 		}
267 | 	}
268 | 	return v
269 | }
270 | 
271 | // ValidateAndAddRequiredInt validates and adds a required integer parameter
272 | func (v *Validator) ValidateAndAddRequiredInt(
273 | 	params map[string]interface{},
274 | 	name string,
275 | ) *Validator {
276 | 	return validateAndAddRequired[int64](v, params, name)
277 | }
278 | 
279 | // ValidateAndAddOptionalInt validates and adds an optional integer parameter
280 | func (v *Validator) ValidateAndAddOptionalInt(
281 | 	params map[string]interface{},
282 | 	name string,
283 | ) *Validator {
284 | 	return validateAndAddOptional[int64](v, params, name)
285 | }
286 | 
287 | // ValidateAndAddRequiredFloat validates and adds a required float parameter
288 | func (v *Validator) ValidateAndAddRequiredFloat(
289 | 	params map[string]interface{},
290 | 	name string,
291 | ) *Validator {
292 | 	return validateAndAddRequired[float64](v, params, name)
293 | }
294 | 
295 | // ValidateAndAddOptionalFloat validates and adds an optional float parameter
296 | func (v *Validator) ValidateAndAddOptionalFloat(
297 | 	params map[string]interface{},
298 | 	name string,
299 | ) *Validator {
300 | 	return validateAndAddOptional[float64](v, params, name)
301 | }
302 | 
303 | // ValidateAndAddRequiredBool validates and adds a required boolean parameter
304 | func (v *Validator) ValidateAndAddRequiredBool(
305 | 	params map[string]interface{},
306 | 	name string,
307 | ) *Validator {
308 | 	return validateAndAddRequired[bool](v, params, name)
309 | }
310 | 
311 | // ValidateAndAddOptionalBool validates and adds an optional boolean parameter
312 | // Note: This adds the boolean value only
313 | // if it was explicitly provided in the request
314 | func (v *Validator) ValidateAndAddOptionalBool(
315 | 	params map[string]interface{},
316 | 	name string,
317 | ) *Validator {
318 | 	// Now validate and add the parameter
319 | 	value, err := extractValueGeneric[bool](v.request, name, false)
320 | 	if err != nil {
321 | 		return v.addError(err)
322 | 	}
323 | 
324 | 	if value == nil {
325 | 		return v
326 | 	}
327 | 
328 | 	params[name] = *value
329 | 	return v
330 | }
331 | 
332 | // validateTokenMaxAmount validates the max_amount field in token.
333 | // max_amount is required and must be a positive number representing
334 | // the maximum amount that can be debited from the customer's account.
335 | func (v *Validator) validateTokenMaxAmount(
336 | 	token map[string]interface{}) *Validator {
337 | 	if maxAmount, exists := token["max_amount"]; exists {
338 | 		switch amt := maxAmount.(type) {
339 | 		case float64:
340 | 			if amt <= 0 {
341 | 				return v.addError(errors.New("token.max_amount must be greater than 0"))
342 | 			}
343 | 		case int:
344 | 			if amt <= 0 {
345 | 				return v.addError(errors.New("token.max_amount must be greater than 0"))
346 | 			}
347 | 			token["max_amount"] = float64(amt) // Convert int to float64
348 | 		default:
349 | 			return v.addError(errors.New("token.max_amount must be a number"))
350 | 		}
351 | 	} else {
352 | 		return v.addError(errors.New("token.max_amount is required"))
353 | 	}
354 | 	return v
355 | }
356 | 
357 | // validateTokenExpireAt validates the expire_at field in token.
358 | // expire_at is optional and defaults to today + 60 days if not provided.
359 | // If provided, it must be a positive Unix timestamp indicating when the
360 | // mandate/token should expire.
361 | func (v *Validator) validateTokenExpireAt(
362 | 	token map[string]interface{}) *Validator {
363 | 	if expireAt, exists := token["expire_at"]; exists {
364 | 		switch exp := expireAt.(type) {
365 | 		case float64:
366 | 			if exp <= 0 {
367 | 				return v.addError(errors.New("token.expire_at must be greater than 0"))
368 | 			}
369 | 		case int:
370 | 			if exp <= 0 {
371 | 				return v.addError(errors.New("token.expire_at must be greater than 0"))
372 | 			}
373 | 			token["expire_at"] = float64(exp) // Convert int to float64
374 | 		default:
375 | 			return v.addError(errors.New("token.expire_at must be a number"))
376 | 		}
377 | 	} else {
378 | 		// Set default value to today + 60 days
379 | 		defaultExpireAt := time.Now().AddDate(0, 0, 60).Unix()
380 | 		token["expire_at"] = float64(defaultExpireAt)
381 | 	}
382 | 	return v
383 | }
384 | 
385 | // validateTokenFrequency validates the frequency field in token.
386 | // frequency is required and must be one of the allowed values:
387 | // "as_presented", "monthly", "one_time", "yearly", "weekly", "daily".
388 | func (v *Validator) validateTokenFrequency(
389 | 	token map[string]interface{}) *Validator {
390 | 	if frequency, exists := token["frequency"]; exists {
391 | 		if freqStr, ok := frequency.(string); ok {
392 | 			validFrequencies := []string{
393 | 				"as_presented", "monthly", "one_time", "yearly", "weekly", "daily"}
394 | 			for _, validFreq := range validFrequencies {
395 | 				if freqStr == validFreq {
396 | 					return v
397 | 				}
398 | 			}
399 | 			return v.addError(errors.New(
400 | 				"token.frequency must be one of: as_presented, " +
401 | 					"monthly, one_time, yearly, weekly, daily"))
402 | 		}
403 | 		return v.addError(errors.New("token.frequency must be a string"))
404 | 	}
405 | 	return v.addError(errors.New("token.frequency is required"))
406 | }
407 | 
408 | // validateTokenType validates the type field in token.
409 | // type is required and must be "single_block_multiple_debit" for SBMD mandates.
410 | func (v *Validator) validateTokenType(token map[string]interface{}) *Validator {
411 | 	if tokenType, exists := token["type"]; exists {
412 | 		if typeStr, ok := tokenType.(string); ok {
413 | 			validTypes := []string{"single_block_multiple_debit"}
414 | 			for _, validType := range validTypes {
415 | 				if typeStr == validType {
416 | 					return v
417 | 				}
418 | 			}
419 | 			return v.addError(errors.New(
420 | 				"token.type must be one of: single_block_multiple_debit"))
421 | 		}
422 | 		return v.addError(errors.New("token.type must be a string"))
423 | 	}
424 | 	return v.addError(errors.New("token.type is required"))
425 | }
426 | 
427 | // ValidateAndAddToken validates and adds a token object with proper structure.
428 | // The token object is used for mandate orders and must contain:
429 | //   - max_amount: positive number (maximum debit amount)
430 | //   - expire_at: optional Unix timestamp (mandate expiry,
431 | //     defaults to today + 60 days)
432 | //   - frequency: string (debit frequency: as_presented, monthly, one_time,
433 | //     yearly, weekly, daily)
434 | //   - type: string (mandate type: single_block_multiple_debit)
435 | func (v *Validator) ValidateAndAddToken(
436 | 	params map[string]interface{}, name string) *Validator {
437 | 	value, err := extractValueGeneric[map[string]interface{}](
438 | 		v.request, name, false)
439 | 	if err != nil {
440 | 		return v.addError(err)
441 | 	}
442 | 
443 | 	if value == nil {
444 | 		return v
445 | 	}
446 | 
447 | 	token := *value
448 | 
449 | 	// Validate all token fields
450 | 	v.validateTokenMaxAmount(token).
451 | 		validateTokenExpireAt(token).
452 | 		validateTokenFrequency(token).
453 | 		validateTokenType(token)
454 | 
455 | 	if v.HasErrors() {
456 | 		return v
457 | 	}
458 | 
459 | 	params[name] = token
460 | 	return v
461 | }
462 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/qr_codes.go:
--------------------------------------------------------------------------------

```go
  1 | package razorpay
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 
  7 | 	rzpsdk "github.com/razorpay/razorpay-go"
  8 | 
  9 | 	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
 10 | 	"github.com/razorpay/razorpay-mcp-server/pkg/observability"
 11 | )
 12 | 
 13 | // CreateQRCode returns a tool that creates QR codes in Razorpay
 14 | func CreateQRCode(
 15 | 	obs *observability.Observability,
 16 | 	client *rzpsdk.Client,
 17 | ) mcpgo.Tool {
 18 | 	parameters := []mcpgo.ToolParameter{
 19 | 		mcpgo.WithString(
 20 | 			"type",
 21 | 			mcpgo.Description(
 22 | 				"The type of the QR Code. Currently only supports 'upi_qr'",
 23 | 			),
 24 | 			mcpgo.Required(),
 25 | 			mcpgo.Pattern("^upi_qr$"),
 26 | 		),
 27 | 		mcpgo.WithString(
 28 | 			"name",
 29 | 			mcpgo.Description(
 30 | 				"Label to identify the QR Code (e.g., 'Store Front Display')",
 31 | 			),
 32 | 		),
 33 | 		mcpgo.WithString(
 34 | 			"usage",
 35 | 			mcpgo.Description(
 36 | 				"Whether QR should accept single or multiple payments. "+
 37 | 					"Possible values: 'single_use', 'multiple_use'",
 38 | 			),
 39 | 			mcpgo.Required(),
 40 | 			mcpgo.Enum("single_use", "multiple_use"),
 41 | 		),
 42 | 		mcpgo.WithBoolean(
 43 | 			"fixed_amount",
 44 | 			mcpgo.Description(
 45 | 				"Whether QR should accept only specific amount (true) or any "+
 46 | 					"amount (false)",
 47 | 			),
 48 | 			mcpgo.DefaultValue(false),
 49 | 		),
 50 | 		mcpgo.WithNumber(
 51 | 			"payment_amount",
 52 | 			mcpgo.Description(
 53 | 				"The specific amount allowed for transaction in smallest "+
 54 | 					"currency unit",
 55 | 			),
 56 | 			mcpgo.Min(1),
 57 | 		),
 58 | 		mcpgo.WithString(
 59 | 			"description",
 60 | 			mcpgo.Description("A brief description about the QR Code"),
 61 | 		),
 62 | 		mcpgo.WithString(
 63 | 			"customer_id",
 64 | 			mcpgo.Description(
 65 | 				"The unique identifier of the customer to link with the QR Code",
 66 | 			),
 67 | 		),
 68 | 		mcpgo.WithNumber(
 69 | 			"close_by",
 70 | 			mcpgo.Description(
 71 | 				"Unix timestamp at which QR Code should be automatically "+
 72 | 					"closed (min 2 mins after current time)",
 73 | 			),
 74 | 		),
 75 | 		mcpgo.WithObject(
 76 | 			"notes",
 77 | 			mcpgo.Description(
 78 | 				"Key-value pairs for additional information "+
 79 | 					"(max 15 pairs, 256 chars each)",
 80 | 			),
 81 | 			mcpgo.MaxProperties(15),
 82 | 		),
 83 | 	}
 84 | 
 85 | 	handler := func(
 86 | 		ctx context.Context,
 87 | 		r mcpgo.CallToolRequest,
 88 | 	) (*mcpgo.ToolResult, error) {
 89 | 		client, err := getClientFromContextOrDefault(ctx, client)
 90 | 		if err != nil {
 91 | 			return mcpgo.NewToolResultError(err.Error()), nil
 92 | 		}
 93 | 
 94 | 		qrData := make(map[string]interface{})
 95 | 
 96 | 		validator := NewValidator(&r).
 97 | 			ValidateAndAddRequiredString(qrData, "type").
 98 | 			ValidateAndAddRequiredString(qrData, "usage").
 99 | 			ValidateAndAddOptionalString(qrData, "name").
100 | 			ValidateAndAddOptionalBool(qrData, "fixed_amount").
101 | 			ValidateAndAddOptionalFloat(qrData, "payment_amount").
102 | 			ValidateAndAddOptionalString(qrData, "description").
103 | 			ValidateAndAddOptionalString(qrData, "customer_id").
104 | 			ValidateAndAddOptionalFloat(qrData, "close_by").
105 | 			ValidateAndAddOptionalMap(qrData, "notes")
106 | 
107 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
108 | 			return result, err
109 | 		}
110 | 
111 | 		// Check if fixed_amount is true, then payment_amount is required
112 | 		if fixedAmount, exists := qrData["fixed_amount"]; exists &&
113 | 			fixedAmount.(bool) {
114 | 			if _, exists := qrData["payment_amount"]; !exists {
115 | 				return mcpgo.NewToolResultError(
116 | 					"payment_amount is required when fixed_amount is true"), nil
117 | 			}
118 | 		}
119 | 
120 | 		// Create QR code using Razorpay SDK
121 | 		qrCode, err := client.QrCode.Create(qrData, nil)
122 | 		if err != nil {
123 | 			return mcpgo.NewToolResultError(
124 | 				fmt.Sprintf("creating QR code failed: %s", err.Error())), nil
125 | 		}
126 | 
127 | 		return mcpgo.NewToolResultJSON(qrCode)
128 | 	}
129 | 
130 | 	return mcpgo.NewTool(
131 | 		"create_qr_code",
132 | 		"Create a new QR code in Razorpay that can be used to accept UPI payments",
133 | 		parameters,
134 | 		handler,
135 | 	)
136 | }
137 | 
138 | // FetchQRCode returns a tool that fetches a specific QR code by ID
139 | func FetchQRCode(
140 | 	obs *observability.Observability,
141 | 	client *rzpsdk.Client,
142 | ) mcpgo.Tool {
143 | 	parameters := []mcpgo.ToolParameter{
144 | 		mcpgo.WithString(
145 | 			"qr_code_id",
146 | 			mcpgo.Description(
147 | 				"Unique identifier of the QR Code to be retrieved"+
148 | 					"The QR code id should start with 'qr_'",
149 | 			),
150 | 			mcpgo.Required(),
151 | 		),
152 | 	}
153 | 
154 | 	handler := func(
155 | 		ctx context.Context,
156 | 		r mcpgo.CallToolRequest,
157 | 	) (*mcpgo.ToolResult, error) {
158 | 		client, err := getClientFromContextOrDefault(ctx, client)
159 | 		if err != nil {
160 | 			return mcpgo.NewToolResultError(err.Error()), nil
161 | 		}
162 | 
163 | 		params := make(map[string]interface{})
164 | 		validator := NewValidator(&r).
165 | 			ValidateAndAddRequiredString(params, "qr_code_id")
166 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
167 | 			return result, err
168 | 		}
169 | 		qrCodeID := params["qr_code_id"].(string)
170 | 
171 | 		// Fetch QR code by ID using Razorpay SDK
172 | 		qrCode, err := client.QrCode.Fetch(qrCodeID, nil, nil)
173 | 		if err != nil {
174 | 			return mcpgo.NewToolResultError(
175 | 				fmt.Sprintf("fetching QR code failed: %s", err.Error())), nil
176 | 		}
177 | 
178 | 		return mcpgo.NewToolResultJSON(qrCode)
179 | 	}
180 | 
181 | 	return mcpgo.NewTool(
182 | 		"fetch_qr_code",
183 | 		"Fetch a QR code's details using it's ID",
184 | 		parameters,
185 | 		handler,
186 | 	)
187 | }
188 | 
189 | // FetchAllQRCodes returns a tool that fetches all QR codes
190 | // with pagination support
191 | func FetchAllQRCodes(
192 | 	obs *observability.Observability,
193 | 	client *rzpsdk.Client,
194 | ) mcpgo.Tool {
195 | 	parameters := []mcpgo.ToolParameter{
196 | 		mcpgo.WithNumber(
197 | 			"from",
198 | 			mcpgo.Description(
199 | 				"Unix timestamp, in seconds, from when QR Codes are to be retrieved",
200 | 			),
201 | 			mcpgo.Min(0),
202 | 		),
203 | 		mcpgo.WithNumber(
204 | 			"to",
205 | 			mcpgo.Description(
206 | 				"Unix timestamp, in seconds, till when QR Codes are to be retrieved",
207 | 			),
208 | 			mcpgo.Min(0),
209 | 		),
210 | 		mcpgo.WithNumber(
211 | 			"count",
212 | 			mcpgo.Description(
213 | 				"Number of QR Codes to be retrieved (default: 10, max: 100)",
214 | 			),
215 | 			mcpgo.Min(1),
216 | 			mcpgo.Max(100),
217 | 		),
218 | 		mcpgo.WithNumber(
219 | 			"skip",
220 | 			mcpgo.Description(
221 | 				"Number of QR Codes to be skipped (default: 0)",
222 | 			),
223 | 			mcpgo.Min(0),
224 | 		),
225 | 	}
226 | 
227 | 	handler := func(
228 | 		ctx context.Context,
229 | 		r mcpgo.CallToolRequest,
230 | 	) (*mcpgo.ToolResult, error) {
231 | 		client, err := getClientFromContextOrDefault(ctx, client)
232 | 		if err != nil {
233 | 			return mcpgo.NewToolResultError(err.Error()), nil
234 | 		}
235 | 
236 | 		fetchQROptions := make(map[string]interface{})
237 | 
238 | 		validator := NewValidator(&r).
239 | 			ValidateAndAddOptionalInt(fetchQROptions, "from").
240 | 			ValidateAndAddOptionalInt(fetchQROptions, "to").
241 | 			ValidateAndAddPagination(fetchQROptions)
242 | 
243 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
244 | 			return result, err
245 | 		}
246 | 
247 | 		// Fetch QR codes using Razorpay SDK
248 | 		qrCodes, err := client.QrCode.All(fetchQROptions, nil)
249 | 		if err != nil {
250 | 			return mcpgo.NewToolResultError(
251 | 				fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
252 | 		}
253 | 
254 | 		return mcpgo.NewToolResultJSON(qrCodes)
255 | 	}
256 | 
257 | 	return mcpgo.NewTool(
258 | 		"fetch_all_qr_codes",
259 | 		"Fetch all QR codes with optional filtering and pagination",
260 | 		parameters,
261 | 		handler,
262 | 	)
263 | }
264 | 
265 | // FetchQRCodesByCustomerID returns a tool that fetches QR codes
266 | // for a specific customer ID
267 | func FetchQRCodesByCustomerID(
268 | 	obs *observability.Observability,
269 | 	client *rzpsdk.Client,
270 | ) mcpgo.Tool {
271 | 	parameters := []mcpgo.ToolParameter{
272 | 		mcpgo.WithString(
273 | 			"customer_id",
274 | 			mcpgo.Description(
275 | 				"The unique identifier of the customer",
276 | 			),
277 | 			mcpgo.Required(),
278 | 		),
279 | 	}
280 | 
281 | 	handler := func(
282 | 		ctx context.Context,
283 | 		r mcpgo.CallToolRequest,
284 | 	) (*mcpgo.ToolResult, error) {
285 | 		client, err := getClientFromContextOrDefault(ctx, client)
286 | 		if err != nil {
287 | 			return mcpgo.NewToolResultError(err.Error()), nil
288 | 		}
289 | 
290 | 		fetchQROptions := make(map[string]interface{})
291 | 
292 | 		validator := NewValidator(&r).
293 | 			ValidateAndAddRequiredString(fetchQROptions, "customer_id")
294 | 
295 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
296 | 			return result, err
297 | 		}
298 | 
299 | 		// Fetch QR codes by customer ID using Razorpay SDK
300 | 		qrCodes, err := client.QrCode.All(fetchQROptions, nil)
301 | 		if err != nil {
302 | 			return mcpgo.NewToolResultError(
303 | 				fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
304 | 		}
305 | 
306 | 		return mcpgo.NewToolResultJSON(qrCodes)
307 | 	}
308 | 
309 | 	return mcpgo.NewTool(
310 | 		"fetch_qr_codes_by_customer_id",
311 | 		"Fetch all QR codes for a specific customer",
312 | 		parameters,
313 | 		handler,
314 | 	)
315 | }
316 | 
317 | // FetchQRCodesByPaymentID returns a tool that fetches QR codes
318 | // for a specific payment ID
319 | func FetchQRCodesByPaymentID(
320 | 	obs *observability.Observability,
321 | 	client *rzpsdk.Client,
322 | ) mcpgo.Tool {
323 | 	parameters := []mcpgo.ToolParameter{
324 | 		mcpgo.WithString(
325 | 			"payment_id",
326 | 			mcpgo.Description(
327 | 				"The unique identifier of the payment"+
328 | 					"The payment id always should start with 'pay_'",
329 | 			),
330 | 			mcpgo.Required(),
331 | 		),
332 | 	}
333 | 
334 | 	handler := func(
335 | 		ctx context.Context,
336 | 		r mcpgo.CallToolRequest,
337 | 	) (*mcpgo.ToolResult, error) {
338 | 		client, err := getClientFromContextOrDefault(ctx, client)
339 | 		if err != nil {
340 | 			return mcpgo.NewToolResultError(err.Error()), nil
341 | 		}
342 | 
343 | 		fetchQROptions := make(map[string]interface{})
344 | 
345 | 		validator := NewValidator(&r).
346 | 			ValidateAndAddRequiredString(fetchQROptions, "payment_id")
347 | 
348 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
349 | 			return result, err
350 | 		}
351 | 
352 | 		// Fetch QR codes by payment ID using Razorpay SDK
353 | 		qrCodes, err := client.QrCode.All(fetchQROptions, nil)
354 | 		if err != nil {
355 | 			return mcpgo.NewToolResultError(
356 | 				fmt.Sprintf("fetching QR codes failed: %s", err.Error())), nil
357 | 		}
358 | 
359 | 		return mcpgo.NewToolResultJSON(qrCodes)
360 | 	}
361 | 
362 | 	return mcpgo.NewTool(
363 | 		"fetch_qr_codes_by_payment_id",
364 | 		"Fetch all QR codes for a specific payment",
365 | 		parameters,
366 | 		handler,
367 | 	)
368 | }
369 | 
370 | // FetchPaymentsForQRCode returns a tool that fetches payments made on a QR code
371 | func FetchPaymentsForQRCode(
372 | 	obs *observability.Observability,
373 | 	client *rzpsdk.Client,
374 | ) mcpgo.Tool {
375 | 	parameters := []mcpgo.ToolParameter{
376 | 		mcpgo.WithString(
377 | 			"qr_code_id",
378 | 			mcpgo.Description(
379 | 				"The unique identifier of the QR Code to fetch payments for"+
380 | 					"The QR code id should start with 'qr_'",
381 | 			),
382 | 			mcpgo.Required(),
383 | 		),
384 | 		mcpgo.WithNumber(
385 | 			"from",
386 | 			mcpgo.Description(
387 | 				"Unix timestamp, in seconds, from when payments are to be retrieved",
388 | 			),
389 | 			mcpgo.Min(0),
390 | 		),
391 | 		mcpgo.WithNumber(
392 | 			"to",
393 | 			mcpgo.Description(
394 | 				"Unix timestamp, in seconds, till when payments are to be fetched",
395 | 			),
396 | 			mcpgo.Min(0),
397 | 		),
398 | 		mcpgo.WithNumber(
399 | 			"count",
400 | 			mcpgo.Description(
401 | 				"Number of payments to be fetched (default: 10, max: 100)",
402 | 			),
403 | 			mcpgo.Min(1),
404 | 			mcpgo.Max(100),
405 | 		),
406 | 		mcpgo.WithNumber(
407 | 			"skip",
408 | 			mcpgo.Description(
409 | 				"Number of records to be skipped while fetching the payments",
410 | 			),
411 | 			mcpgo.Min(0),
412 | 		),
413 | 	}
414 | 
415 | 	handler := func(
416 | 		ctx context.Context,
417 | 		r mcpgo.CallToolRequest,
418 | 	) (*mcpgo.ToolResult, error) {
419 | 		client, err := getClientFromContextOrDefault(ctx, client)
420 | 		if err != nil {
421 | 			return mcpgo.NewToolResultError(err.Error()), nil
422 | 		}
423 | 
424 | 		params := make(map[string]interface{})
425 | 		fetchQROptions := make(map[string]interface{})
426 | 
427 | 		validator := NewValidator(&r).
428 | 			ValidateAndAddRequiredString(params, "qr_code_id").
429 | 			ValidateAndAddOptionalInt(fetchQROptions, "from").
430 | 			ValidateAndAddOptionalInt(fetchQROptions, "to").
431 | 			ValidateAndAddOptionalInt(fetchQROptions, "count").
432 | 			ValidateAndAddOptionalInt(fetchQROptions, "skip")
433 | 
434 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
435 | 			return result, err
436 | 		}
437 | 
438 | 		qrCodeID := params["qr_code_id"].(string)
439 | 
440 | 		// Fetch payments for QR code using Razorpay SDK
441 | 		payments, err := client.QrCode.FetchPayments(qrCodeID, fetchQROptions, nil)
442 | 		if err != nil {
443 | 			return mcpgo.NewToolResultError(
444 | 				fmt.Sprintf("fetching payments for QR code failed: %s", err.Error())), nil
445 | 		}
446 | 
447 | 		return mcpgo.NewToolResultJSON(payments)
448 | 	}
449 | 
450 | 	return mcpgo.NewTool(
451 | 		"fetch_payments_for_qr_code",
452 | 		"Fetch all payments made on a QR code",
453 | 		parameters,
454 | 		handler,
455 | 	)
456 | }
457 | 
458 | // CloseQRCode returns a tool that closes a specific QR code
459 | func CloseQRCode(
460 | 	obs *observability.Observability,
461 | 	client *rzpsdk.Client,
462 | ) mcpgo.Tool {
463 | 	parameters := []mcpgo.ToolParameter{
464 | 		mcpgo.WithString(
465 | 			"qr_code_id",
466 | 			mcpgo.Description(
467 | 				"Unique identifier of the QR Code to be closed"+
468 | 					"The QR code id should start with 'qr_'",
469 | 			),
470 | 			mcpgo.Required(),
471 | 		),
472 | 	}
473 | 
474 | 	handler := func(
475 | 		ctx context.Context,
476 | 		r mcpgo.CallToolRequest,
477 | 	) (*mcpgo.ToolResult, error) {
478 | 		client, err := getClientFromContextOrDefault(ctx, client)
479 | 		if err != nil {
480 | 			return mcpgo.NewToolResultError(err.Error()), nil
481 | 		}
482 | 
483 | 		params := make(map[string]interface{})
484 | 		validator := NewValidator(&r).
485 | 			ValidateAndAddRequiredString(params, "qr_code_id")
486 | 		if result, err := validator.HandleErrorsIfAny(); result != nil {
487 | 			return result, err
488 | 		}
489 | 		qrCodeID := params["qr_code_id"].(string)
490 | 
491 | 		// Close QR code by ID using Razorpay SDK
492 | 		qrCode, err := client.QrCode.Close(qrCodeID, nil, nil)
493 | 		if err != nil {
494 | 			return mcpgo.NewToolResultError(
495 | 				fmt.Sprintf("closing QR code failed: %s", err.Error())), nil
496 | 		}
497 | 
498 | 		return mcpgo.NewToolResultJSON(qrCode)
499 | 	}
500 | 
501 | 	return mcpgo.NewTool(
502 | 		"close_qr_code",
503 | 		"Close a QR Code that's no longer needed",
504 | 		parameters,
505 | 		handler,
506 | 	)
507 | }
508 | 
```

--------------------------------------------------------------------------------
/pkg/mcpgo/tool.go:
--------------------------------------------------------------------------------

```go
  1 | package mcpgo
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 
  7 | 	"github.com/mark3labs/mcp-go/mcp"
  8 | 	"github.com/mark3labs/mcp-go/server"
  9 | )
 10 | 
 11 | // ToolHandler handles tool calls
 12 | type ToolHandler func(
 13 | 	ctx context.Context,
 14 | 	request CallToolRequest) (*ToolResult, error)
 15 | 
 16 | // CallToolRequest represents a request to call a tool
 17 | type CallToolRequest struct {
 18 | 	Name      string
 19 | 	Arguments any
 20 | }
 21 | 
 22 | // ToolResult represents the result of a tool call
 23 | type ToolResult struct {
 24 | 	Text    string
 25 | 	IsError bool
 26 | 	Content []interface{}
 27 | }
 28 | 
 29 | // Tool represents a tool that can be added to the server
 30 | type Tool interface {
 31 | 	// internal method to convert to mcp's ServerTool
 32 | 	toMCPServerTool() server.ServerTool
 33 | 
 34 | 	// GetHandler internal method for fetching the underlying handler
 35 | 	GetHandler() ToolHandler
 36 | }
 37 | 
 38 | // PropertyOption represents a customization option for
 39 | // a parameter's schema
 40 | type PropertyOption func(schema map[string]interface{})
 41 | 
 42 | // Min sets the minimum value for a number parameter or
 43 | // minimum length for a string
 44 | func Min(value float64) PropertyOption {
 45 | 	return func(schema map[string]interface{}) {
 46 | 		propType, ok := schema["type"].(string)
 47 | 		if !ok {
 48 | 			return
 49 | 		}
 50 | 
 51 | 		switch propType {
 52 | 		case "number", "integer":
 53 | 			schema["minimum"] = value
 54 | 		case "string":
 55 | 			schema["minLength"] = int(value)
 56 | 		case "array":
 57 | 			schema["minItems"] = int(value)
 58 | 		}
 59 | 	}
 60 | }
 61 | 
 62 | // Max sets the maximum value for a number parameter or
 63 | // maximum length for a string
 64 | func Max(value float64) PropertyOption {
 65 | 	return func(schema map[string]interface{}) {
 66 | 		propType, ok := schema["type"].(string)
 67 | 		if !ok {
 68 | 			return
 69 | 		}
 70 | 
 71 | 		switch propType {
 72 | 		case "number", "integer":
 73 | 			schema["maximum"] = value
 74 | 		case "string":
 75 | 			schema["maxLength"] = int(value)
 76 | 		case "array":
 77 | 			schema["maxItems"] = int(value)
 78 | 		}
 79 | 	}
 80 | }
 81 | 
 82 | // Pattern sets a regex pattern for string validation
 83 | func Pattern(pattern string) PropertyOption {
 84 | 	return func(schema map[string]interface{}) {
 85 | 		propType, ok := schema["type"].(string)
 86 | 		if !ok || propType != "string" {
 87 | 			return
 88 | 		}
 89 | 		schema["pattern"] = pattern
 90 | 	}
 91 | }
 92 | 
 93 | // Enum sets allowed values for a parameter
 94 | func Enum(values ...interface{}) PropertyOption {
 95 | 	return func(schema map[string]interface{}) {
 96 | 		schema["enum"] = values
 97 | 	}
 98 | }
 99 | 
100 | // DefaultValue sets a default value for a parameter
101 | func DefaultValue(value interface{}) PropertyOption {
102 | 	return func(schema map[string]interface{}) {
103 | 		schema["default"] = value
104 | 	}
105 | }
106 | 
107 | // MaxProperties sets the maximum number of properties for an object
108 | func MaxProperties(max int) PropertyOption {
109 | 	return func(schema map[string]interface{}) {
110 | 		propType, ok := schema["type"].(string)
111 | 		if !ok || propType != "object" {
112 | 			return
113 | 		}
114 | 		schema["maxProperties"] = max
115 | 	}
116 | }
117 | 
118 | // MinProperties sets the minimum number of properties for an object
119 | func MinProperties(min int) PropertyOption {
120 | 	return func(schema map[string]interface{}) {
121 | 		propType, ok := schema["type"].(string)
122 | 		if !ok || propType != "object" {
123 | 			return
124 | 		}
125 | 		schema["minProperties"] = min
126 | 	}
127 | }
128 | 
129 | // Required sets the tool parameter as required.
130 | // When a parameter is marked as required, the client must provide a value
131 | // for this parameter or the tool call will fail with an error.
132 | func Required() PropertyOption {
133 | 	return func(schema map[string]interface{}) {
134 | 		schema["required"] = true
135 | 	}
136 | }
137 | 
138 | // Description sets the description for the tool parameter.
139 | // The description should explain the purpose of the parameter, expected format,
140 | // and any relevant constraints.
141 | func Description(desc string) PropertyOption {
142 | 	return func(schema map[string]interface{}) {
143 | 		schema["description"] = desc
144 | 	}
145 | }
146 | 
147 | // ToolParameter represents a parameter for a tool
148 | type ToolParameter struct {
149 | 	Name   string
150 | 	Schema map[string]interface{}
151 | }
152 | 
153 | // applyPropertyOptions applies the given property options to
154 | // the parameter schema
155 | func (p *ToolParameter) applyPropertyOptions(opts ...PropertyOption) {
156 | 	for _, opt := range opts {
157 | 		opt(p.Schema)
158 | 	}
159 | }
160 | 
161 | // WithString creates a string parameter with optional property options
162 | func WithString(name string, opts ...PropertyOption) ToolParameter {
163 | 	param := ToolParameter{
164 | 		Name:   name,
165 | 		Schema: map[string]interface{}{"type": "string"},
166 | 	}
167 | 	param.applyPropertyOptions(opts...)
168 | 	return param
169 | }
170 | 
171 | // WithNumber creates a number parameter with optional property options
172 | func WithNumber(name string, opts ...PropertyOption) ToolParameter {
173 | 	param := ToolParameter{
174 | 		Name:   name,
175 | 		Schema: map[string]interface{}{"type": "number"},
176 | 	}
177 | 	param.applyPropertyOptions(opts...)
178 | 	return param
179 | }
180 | 
181 | // WithBoolean creates a boolean parameter with optional property options
182 | func WithBoolean(name string, opts ...PropertyOption) ToolParameter {
183 | 	param := ToolParameter{
184 | 		Name:   name,
185 | 		Schema: map[string]interface{}{"type": "boolean"},
186 | 	}
187 | 	param.applyPropertyOptions(opts...)
188 | 	return param
189 | }
190 | 
191 | // WithObject creates an object parameter with optional property options
192 | func WithObject(name string, opts ...PropertyOption) ToolParameter {
193 | 	param := ToolParameter{
194 | 		Name:   name,
195 | 		Schema: map[string]interface{}{"type": "object"},
196 | 	}
197 | 	param.applyPropertyOptions(opts...)
198 | 	return param
199 | }
200 | 
201 | // WithArray creates an array parameter with optional property options
202 | func WithArray(name string, opts ...PropertyOption) ToolParameter {
203 | 	param := ToolParameter{
204 | 		Name:   name,
205 | 		Schema: map[string]interface{}{"type": "array"},
206 | 	}
207 | 	param.applyPropertyOptions(opts...)
208 | 	return param
209 | }
210 | 
211 | // mark3labsToolImpl implements the Tool interface
212 | type mark3labsToolImpl struct {
213 | 	name        string
214 | 	description string
215 | 	handler     ToolHandler
216 | 	parameters  []ToolParameter
217 | }
218 | 
219 | // NewTool creates a new tool with the given
220 | // Name, description, parameters and handler
221 | func NewTool(
222 | 	name,
223 | 	description string,
224 | 	parameters []ToolParameter,
225 | 	handler ToolHandler) *mark3labsToolImpl {
226 | 	return &mark3labsToolImpl{
227 | 		name:        name,
228 | 		description: description,
229 | 		handler:     handler,
230 | 		parameters:  parameters,
231 | 	}
232 | }
233 | 
234 | // addNumberPropertyOptions adds number-specific options to the property options
235 | func addNumberPropertyOptions(
236 | 	propOpts []mcp.PropertyOption,
237 | 	schema map[string]interface{}) []mcp.PropertyOption {
238 | 	// Add minimum if present
239 | 	if min, ok := schema["minimum"].(float64); ok {
240 | 		propOpts = append(propOpts, mcp.Min(min))
241 | 	}
242 | 
243 | 	// Add maximum if present
244 | 	if max, ok := schema["maximum"].(float64); ok {
245 | 		propOpts = append(propOpts, mcp.Max(max))
246 | 	}
247 | 
248 | 	return propOpts
249 | }
250 | 
251 | // addStringPropertyOptions adds string-specific options to the property options
252 | func addStringPropertyOptions(
253 | 	propOpts []mcp.PropertyOption,
254 | 	schema map[string]interface{}) []mcp.PropertyOption {
255 | 	// Add minLength if present
256 | 	if minLength, ok := schema["minLength"].(int); ok {
257 | 		propOpts = append(propOpts, mcp.MinLength(minLength))
258 | 	}
259 | 
260 | 	// Add maxLength if present
261 | 	if maxLength, ok := schema["maxLength"].(int); ok {
262 | 		propOpts = append(propOpts, mcp.MaxLength(maxLength))
263 | 	}
264 | 
265 | 	// Add pattern if present
266 | 	if pattern, ok := schema["pattern"].(string); ok {
267 | 		propOpts = append(propOpts, mcp.Pattern(pattern))
268 | 	}
269 | 
270 | 	return propOpts
271 | }
272 | 
273 | // addDefaultValueOptions adds default value options based on type
274 | func addDefaultValueOptions(
275 | 	propOpts []mcp.PropertyOption,
276 | 	defaultValue interface{}) []mcp.PropertyOption {
277 | 	switch val := defaultValue.(type) {
278 | 	case string:
279 | 		propOpts = append(propOpts, mcp.DefaultString(val))
280 | 	case float64:
281 | 		propOpts = append(propOpts, mcp.DefaultNumber(val))
282 | 	case bool:
283 | 		propOpts = append(propOpts, mcp.DefaultBool(val))
284 | 	}
285 | 	return propOpts
286 | }
287 | 
288 | // addEnumOptions adds enum options if present
289 | func addEnumOptions(
290 | 	propOpts []mcp.PropertyOption,
291 | 	enumValues interface{}) []mcp.PropertyOption {
292 | 	values, ok := enumValues.([]interface{})
293 | 	if !ok {
294 | 		return propOpts
295 | 	}
296 | 
297 | 	// Convert values to strings for now
298 | 	strValues := make([]string, 0, len(values))
299 | 	for _, ev := range values {
300 | 		if str, ok := ev.(string); ok {
301 | 			strValues = append(strValues, str)
302 | 		}
303 | 	}
304 | 
305 | 	if len(strValues) > 0 {
306 | 		propOpts = append(propOpts, mcp.Enum(strValues...))
307 | 	}
308 | 
309 | 	return propOpts
310 | }
311 | 
312 | // addObjectPropertyOptions adds object-specific options
313 | func addObjectPropertyOptions(
314 | 	propOpts []mcp.PropertyOption,
315 | 	schema map[string]interface{}) []mcp.PropertyOption {
316 | 	// Add maxProperties if present
317 | 	if maxProps, ok := schema["maxProperties"].(int); ok {
318 | 		propOpts = append(propOpts, mcp.MaxProperties(maxProps))
319 | 	}
320 | 
321 | 	// Add minProperties if present
322 | 	if minProps, ok := schema["minProperties"].(int); ok {
323 | 		propOpts = append(propOpts, mcp.MinProperties(minProps))
324 | 	}
325 | 
326 | 	return propOpts
327 | }
328 | 
329 | // addArrayPropertyOptions adds array-specific options
330 | func addArrayPropertyOptions(
331 | 	propOpts []mcp.PropertyOption,
332 | 	schema map[string]interface{}) []mcp.PropertyOption {
333 | 	// Add minItems if present
334 | 	if minItems, ok := schema["minItems"].(int); ok {
335 | 		propOpts = append(propOpts, mcp.MinItems(minItems))
336 | 	}
337 | 
338 | 	// Add maxItems if present
339 | 	if maxItems, ok := schema["maxItems"].(int); ok {
340 | 		propOpts = append(propOpts, mcp.MaxItems(maxItems))
341 | 	}
342 | 
343 | 	return propOpts
344 | }
345 | 
346 | // convertSchemaToPropertyOptions converts our schema to mcp property options
347 | func convertSchemaToPropertyOptions(
348 | 	schema map[string]interface{}) []mcp.PropertyOption {
349 | 	var propOpts []mcp.PropertyOption
350 | 
351 | 	// Add description if present
352 | 	if description, ok := schema["description"].(string); ok && description != "" {
353 | 		propOpts = append(propOpts, mcp.Description(description))
354 | 	}
355 | 
356 | 	// Add required flag if present
357 | 	if required, ok := schema["required"].(bool); ok && required {
358 | 		propOpts = append(propOpts, mcp.Required())
359 | 	}
360 | 
361 | 	// Skip type, description and required as they're handled separately
362 | 	for k, v := range schema {
363 | 		if k == "type" || k == "description" || k == "required" {
364 | 			continue
365 | 		}
366 | 
367 | 		// Process property based on key
368 | 		switch k {
369 | 		case "minimum", "maximum":
370 | 			propOpts = addNumberPropertyOptions(propOpts, schema)
371 | 		case "minLength", "maxLength", "pattern":
372 | 			propOpts = addStringPropertyOptions(propOpts, schema)
373 | 		case "default":
374 | 			propOpts = addDefaultValueOptions(propOpts, v)
375 | 		case "enum":
376 | 			propOpts = addEnumOptions(propOpts, v)
377 | 		case "maxProperties", "minProperties":
378 | 			propOpts = addObjectPropertyOptions(propOpts, schema)
379 | 		case "minItems", "maxItems":
380 | 			propOpts = addArrayPropertyOptions(propOpts, schema)
381 | 		}
382 | 	}
383 | 
384 | 	return propOpts
385 | }
386 | 
387 | // GetHandler returns the handler for the tool
388 | func (t *mark3labsToolImpl) GetHandler() ToolHandler {
389 | 	return t.handler
390 | }
391 | 
392 | // toMCPServerTool converts our Tool to mcp's ServerTool
393 | func (t *mark3labsToolImpl) toMCPServerTool() server.ServerTool {
394 | 	// Create the mcp tool with appropriate options
395 | 	var toolOpts []mcp.ToolOption
396 | 
397 | 	// Add description
398 | 	toolOpts = append(toolOpts, mcp.WithDescription(t.description))
399 | 
400 | 	// Add parameters with their schemas
401 | 	for _, param := range t.parameters {
402 | 		// Get property options from schema
403 | 		propOpts := convertSchemaToPropertyOptions(param.Schema)
404 | 
405 | 		// Get the type from the schema
406 | 		schemaType, ok := param.Schema["type"].(string)
407 | 		if !ok {
408 | 			// Default to string if type is missing or not a string
409 | 			schemaType = "string"
410 | 		}
411 | 
412 | 		// Use the appropriate function based on schema type
413 | 		switch schemaType {
414 | 		case "string":
415 | 			toolOpts = append(toolOpts, mcp.WithString(param.Name, propOpts...))
416 | 		case "number", "integer":
417 | 			toolOpts = append(toolOpts, mcp.WithNumber(param.Name, propOpts...))
418 | 		case "boolean":
419 | 			toolOpts = append(toolOpts, mcp.WithBoolean(param.Name, propOpts...))
420 | 		case "object":
421 | 			toolOpts = append(toolOpts, mcp.WithObject(param.Name, propOpts...))
422 | 		case "array":
423 | 			toolOpts = append(toolOpts, mcp.WithArray(param.Name, propOpts...))
424 | 		default:
425 | 			// Unknown type, default to string
426 | 			toolOpts = append(toolOpts, mcp.WithString(param.Name, propOpts...))
427 | 		}
428 | 	}
429 | 
430 | 	// Create the tool with all options
431 | 	tool := mcp.NewTool(t.name, toolOpts...)
432 | 
433 | 	// Create the handler
434 | 	handlerFunc := func(
435 | 		ctx context.Context,
436 | 		req mcp.CallToolRequest,
437 | 	) (*mcp.CallToolResult, error) {
438 | 		// Convert mcp request to our request
439 | 		ourReq := CallToolRequest{
440 | 			Name:      req.Params.Name,
441 | 			Arguments: req.Params.Arguments,
442 | 		}
443 | 
444 | 		// Call our handler
445 | 		result, err := t.handler(ctx, ourReq)
446 | 		if err != nil {
447 | 			return nil, err
448 | 		}
449 | 
450 | 		// Convert our result to mcp result
451 | 		var mcpResult *mcp.CallToolResult
452 | 		if result.IsError {
453 | 			mcpResult = mcp.NewToolResultError(result.Text)
454 | 		} else {
455 | 			mcpResult = mcp.NewToolResultText(result.Text)
456 | 		}
457 | 
458 | 		return mcpResult, nil
459 | 	}
460 | 
461 | 	return server.ServerTool{
462 | 		Tool:    tool,
463 | 		Handler: handlerFunc,
464 | 	}
465 | }
466 | 
467 | // NewToolResultJSON creates a new tool result with JSON content
468 | func NewToolResultJSON(data interface{}) (*ToolResult, error) {
469 | 	jsonBytes, err := json.Marshal(data)
470 | 	if err != nil {
471 | 		return nil, err
472 | 	}
473 | 
474 | 	return &ToolResult{
475 | 		Text:    string(jsonBytes),
476 | 		IsError: false,
477 | 		Content: nil,
478 | 	}, nil
479 | }
480 | 
481 | // NewToolResultText creates a new tool result with text content
482 | func NewToolResultText(text string) *ToolResult {
483 | 	return &ToolResult{
484 | 		Text:    text,
485 | 		IsError: false,
486 | 		Content: nil,
487 | 	}
488 | }
489 | 
490 | // NewToolResultError creates a new tool result with an error
491 | func NewToolResultError(text string) *ToolResult {
492 | 	return &ToolResult{
493 | 		Text:    text,
494 | 		IsError: true,
495 | 		Content: nil,
496 | 	}
497 | }
498 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/tokens_test.go:
--------------------------------------------------------------------------------

```go
  1 | package razorpay
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"net/http"
  7 | 	"net/http/httptest"
  8 | 	"strings"
  9 | 	"testing"
 10 | 
 11 | 	"github.com/razorpay/razorpay-go/constants"
 12 | 
 13 | 	"github.com/razorpay/razorpay-mcp-server/pkg/contextkey"
 14 | 	"github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
 15 | 	"github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
 16 | )
 17 | 
 18 | func Test_FetchSavedPaymentMethods(t *testing.T) {
 19 | 	// URL patterns for mocking
 20 | 	createCustomerPath := fmt.Sprintf(
 21 | 		"/%s%s",
 22 | 		constants.VERSION_V1,
 23 | 		constants.CUSTOMER_URL,
 24 | 	)
 25 | 
 26 | 	fetchTokensPathFmt := fmt.Sprintf(
 27 | 		"/%s/customers/%%s/tokens",
 28 | 		constants.VERSION_V1,
 29 | 	)
 30 | 
 31 | 	// Sample successful customer creation/fetch response
 32 | 	customerResp := map[string]interface{}{
 33 | 		"id":         "cust_1Aa00000000003",
 34 | 		"entity":     "customer",
 35 | 		"name":       "",
 36 | 		"email":      "",
 37 | 		"contact":    "9876543210",
 38 | 		"gstin":      nil,
 39 | 		"notes":      map[string]interface{}{},
 40 | 		"created_at": float64(1234567890),
 41 | 	}
 42 | 
 43 | 	// Sample successful tokens response
 44 | 	tokensResp := map[string]interface{}{
 45 | 		"entity": "collection",
 46 | 		"count":  float64(2),
 47 | 		"items": []interface{}{
 48 | 			map[string]interface{}{
 49 | 				"id":     "token_ABCDEFGH",
 50 | 				"entity": "token",
 51 | 				"token":  "EhYXHrLsJdwRhM",
 52 | 				"bank":   nil,
 53 | 				"wallet": nil,
 54 | 				"method": "card",
 55 | 				"card": map[string]interface{}{
 56 | 					"entity":        "card",
 57 | 					"name":          "Gaurav Kumar",
 58 | 					"last4":         "1111",
 59 | 					"network":       "Visa",
 60 | 					"type":          "debit",
 61 | 					"issuer":        "HDFC",
 62 | 					"international": false,
 63 | 					"emi":           false,
 64 | 					"sub_type":      "consumer",
 65 | 				},
 66 | 				"vpa":       nil,
 67 | 				"recurring": true,
 68 | 				"recurring_details": map[string]interface{}{
 69 | 					"status":         "confirmed",
 70 | 					"failure_reason": nil,
 71 | 				},
 72 | 				"auth_type":   nil,
 73 | 				"mrn":         nil,
 74 | 				"used_at":     float64(1629779657),
 75 | 				"created_at":  float64(1629779657),
 76 | 				"expired_at":  float64(1640918400),
 77 | 				"dcc_enabled": false,
 78 | 			},
 79 | 			map[string]interface{}{
 80 | 				"id":     "token_EhYXHrLsJdwRhN",
 81 | 				"entity": "token",
 82 | 				"token":  "EhYXHrLsJdwRhN",
 83 | 				"bank":   nil,
 84 | 				"wallet": nil,
 85 | 				"method": "upi",
 86 | 				"card":   nil,
 87 | 				"vpa": map[string]interface{}{
 88 | 					"username": "gauravkumar",
 89 | 					"handle":   "okhdfcbank",
 90 | 					"name":     "Gaurav Kumar",
 91 | 				},
 92 | 				"recurring": true,
 93 | 				"recurring_details": map[string]interface{}{
 94 | 					"status":         "confirmed",
 95 | 					"failure_reason": nil,
 96 | 				},
 97 | 				"auth_type":   nil,
 98 | 				"mrn":         nil,
 99 | 				"used_at":     float64(1629779657),
100 | 				"created_at":  float64(1629779657),
101 | 				"expired_at":  float64(1640918400),
102 | 				"dcc_enabled": false,
103 | 			},
104 | 		},
105 | 	}
106 | 
107 | 	// Expected combined response
108 | 	expectedSuccessResp := map[string]interface{}{
109 | 		"customer":              customerResp,
110 | 		"saved_payment_methods": tokensResp,
111 | 	}
112 | 
113 | 	// Error responses
114 | 	customerCreationFailedResp := map[string]interface{}{
115 | 		"error": map[string]interface{}{
116 | 			"code":        "BAD_REQUEST_ERROR",
117 | 			"description": "Contact number is invalid",
118 | 		},
119 | 	}
120 | 
121 | 	tokensAPIFailedResp := map[string]interface{}{
122 | 		"error": map[string]interface{}{
123 | 			"code":        "BAD_REQUEST_ERROR",
124 | 			"description": "Customer not found",
125 | 		},
126 | 	}
127 | 
128 | 	// Customer response without ID (invalid)
129 | 	invalidCustomerResp := map[string]interface{}{
130 | 		"entity":     "customer",
131 | 		"name":       "",
132 | 		"email":      "",
133 | 		"contact":    "9876543210",
134 | 		"gstin":      nil,
135 | 		"notes":      map[string]interface{}{},
136 | 		"created_at": float64(1234567890),
137 | 		// Missing "id" field
138 | 	}
139 | 
140 | 	tests := []RazorpayToolTestCase{
141 | 		{
142 | 			Name: "successful fetch of saved cards with valid contact",
143 | 			Request: map[string]interface{}{
144 | 				"contact": "9876543210",
145 | 			},
146 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
147 | 				return mock.NewHTTPClient(
148 | 					mock.Endpoint{
149 | 						Path:     createCustomerPath,
150 | 						Method:   "POST",
151 | 						Response: customerResp,
152 | 					},
153 | 					mock.Endpoint{
154 | 						Path:     fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"),
155 | 						Method:   "GET",
156 | 						Response: tokensResp,
157 | 					},
158 | 				)
159 | 			},
160 | 			ExpectError:    false,
161 | 			ExpectedResult: expectedSuccessResp,
162 | 		},
163 | 		{
164 | 			Name: "successful fetch with international contact format",
165 | 			Request: map[string]interface{}{
166 | 				"contact": "+919876543210",
167 | 			},
168 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
169 | 				customerRespIntl := map[string]interface{}{
170 | 					"id":         "cust_1Aa00000000004",
171 | 					"entity":     "customer",
172 | 					"name":       "",
173 | 					"email":      "",
174 | 					"contact":    "+919876543210",
175 | 					"gstin":      nil,
176 | 					"notes":      map[string]interface{}{},
177 | 					"created_at": float64(1234567890),
178 | 				}
179 | 				return mock.NewHTTPClient(
180 | 					mock.Endpoint{
181 | 						Path:     createCustomerPath,
182 | 						Method:   "POST",
183 | 						Response: customerRespIntl,
184 | 					},
185 | 					mock.Endpoint{
186 | 						Path:     fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000004"),
187 | 						Method:   "GET",
188 | 						Response: tokensResp,
189 | 					},
190 | 				)
191 | 			},
192 | 			ExpectError: false,
193 | 			ExpectedResult: map[string]interface{}{
194 | 				"customer": map[string]interface{}{
195 | 					"id":         "cust_1Aa00000000004",
196 | 					"entity":     "customer",
197 | 					"name":       "",
198 | 					"email":      "",
199 | 					"contact":    "+919876543210",
200 | 					"gstin":      nil,
201 | 					"notes":      map[string]interface{}{},
202 | 					"created_at": float64(1234567890),
203 | 				},
204 | 				"saved_payment_methods": tokensResp,
205 | 			},
206 | 		},
207 | 		{
208 | 			Name: "customer creation/fetch failure",
209 | 			Request: map[string]interface{}{
210 | 				"contact": "invalid_contact",
211 | 			},
212 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
213 | 				return mock.NewHTTPClient(
214 | 					mock.Endpoint{
215 | 						Path:     createCustomerPath,
216 | 						Method:   "POST",
217 | 						Response: customerCreationFailedResp,
218 | 					},
219 | 				)
220 | 			},
221 | 			ExpectError: true,
222 | 			ExpectedErrMsg: "Failed to create/fetch customer with " +
223 | 				"contact invalid_contact: Contact number is invalid",
224 | 		},
225 | 		{
226 | 			Name: "tokens API failure after successful customer creation",
227 | 			Request: map[string]interface{}{
228 | 				"contact": "9876543210",
229 | 			},
230 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
231 | 				return mock.NewHTTPClient(
232 | 					mock.Endpoint{
233 | 						Path:     createCustomerPath,
234 | 						Method:   "POST",
235 | 						Response: customerResp,
236 | 					},
237 | 					mock.Endpoint{
238 | 						Path:     fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"),
239 | 						Method:   "GET",
240 | 						Response: tokensAPIFailedResp,
241 | 					},
242 | 				)
243 | 			},
244 | 			ExpectError: true,
245 | 			ExpectedErrMsg: "Failed to fetch saved payment methods for " +
246 | 				"customer cust_1Aa00000000003: Customer not found",
247 | 		},
248 | 		{
249 | 			Name: "invalid customer response - missing customer ID",
250 | 			Request: map[string]interface{}{
251 | 				"contact": "9876543210",
252 | 			},
253 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
254 | 				return mock.NewHTTPClient(
255 | 					mock.Endpoint{
256 | 						Path:     createCustomerPath,
257 | 						Method:   "POST",
258 | 						Response: invalidCustomerResp,
259 | 					},
260 | 				)
261 | 			},
262 | 			ExpectError:    true,
263 | 			ExpectedErrMsg: "Customer ID not found in response",
264 | 		},
265 | 		{
266 | 			Name:    "missing contact parameter",
267 | 			Request: map[string]interface{}{
268 | 				// No contact parameter
269 | 			},
270 | 			MockHttpClient: nil, // No HTTP client needed for validation error
271 | 			ExpectError:    true,
272 | 			ExpectedErrMsg: "missing required parameter: contact",
273 | 		},
274 | 		{
275 | 			Name: "empty contact parameter",
276 | 			Request: map[string]interface{}{
277 | 				"contact": "",
278 | 			},
279 | 			MockHttpClient: nil, // No HTTP client needed for validation error
280 | 			ExpectError:    true,
281 | 			ExpectedErrMsg: "missing required parameter: contact",
282 | 		},
283 | 		{
284 | 			Name: "null contact parameter",
285 | 			Request: map[string]interface{}{
286 | 				"contact": nil,
287 | 			},
288 | 			MockHttpClient: nil, // No HTTP client needed for validation error
289 | 			ExpectError:    true,
290 | 			ExpectedErrMsg: "missing required parameter: contact",
291 | 		},
292 | 		{
293 | 			Name: "successful fetch with empty tokens list",
294 | 			Request: map[string]interface{}{
295 | 				"contact": "9876543210",
296 | 			},
297 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
298 | 				emptyTokensResp := map[string]interface{}{
299 | 					"entity": "collection",
300 | 					"count":  float64(0),
301 | 					"items":  []interface{}{},
302 | 				}
303 | 				return mock.NewHTTPClient(
304 | 					mock.Endpoint{
305 | 						Path:     createCustomerPath,
306 | 						Method:   "POST",
307 | 						Response: customerResp,
308 | 					},
309 | 					mock.Endpoint{
310 | 						Path:     fmt.Sprintf(fetchTokensPathFmt, "cust_1Aa00000000003"),
311 | 						Method:   "GET",
312 | 						Response: emptyTokensResp,
313 | 					},
314 | 				)
315 | 			},
316 | 			ExpectError: false,
317 | 			ExpectedResult: map[string]interface{}{
318 | 				"customer": customerResp,
319 | 				"saved_payment_methods": map[string]interface{}{
320 | 					"entity": "collection",
321 | 					"count":  float64(0),
322 | 					"items":  []interface{}{},
323 | 				},
324 | 			},
325 | 		},
326 | 	}
327 | 
328 | 	for _, tc := range tests {
329 | 		t.Run(tc.Name, func(t *testing.T) {
330 | 			runToolTest(t, tc, FetchSavedPaymentMethods, "Saved Cards")
331 | 		})
332 | 	}
333 | }
334 | 
335 | // Test_FetchSavedPaymentMethods_ClientContextScenarios tests scenarios
336 | // related to client context handling for 100% code coverage
337 | func Test_FetchSavedPaymentMethods_ClientContextScenarios(t *testing.T) {
338 | 	obs := CreateTestObservability()
339 | 
340 | 	t.Run("no client in context and default is nil", func(t *testing.T) {
341 | 		// Create tool with nil client
342 | 		tool := FetchSavedPaymentMethods(obs, nil)
343 | 
344 | 		// Create context without client
345 | 		ctx := context.Background()
346 | 		request := mcpgo.CallToolRequest{
347 | 			Arguments: map[string]interface{}{
348 | 				"contact": "9876543210",
349 | 			},
350 | 		}
351 | 
352 | 		result, err := tool.GetHandler()(ctx, request)
353 | 
354 | 		if err != nil {
355 | 			t.Fatalf("Expected no error, got %v", err)
356 | 		}
357 | 
358 | 		if result == nil {
359 | 			t.Fatal("Expected result, got nil")
360 | 		}
361 | 
362 | 		if result.Text == "" {
363 | 			t.Fatal("Expected error message in result")
364 | 		}
365 | 
366 | 		expectedErrMsg := "no client found in context"
367 | 		if !strings.Contains(result.Text, expectedErrMsg) {
368 | 			t.Errorf(
369 | 				"Expected error message to contain '%s', got '%s'",
370 | 				expectedErrMsg,
371 | 				result.Text,
372 | 			)
373 | 		}
374 | 	})
375 | 
376 | 	t.Run("invalid client type in context", func(t *testing.T) {
377 | 		// Create tool with nil client
378 | 		tool := FetchSavedPaymentMethods(obs, nil)
379 | 
380 | 		// Create context with invalid client type
381 | 		ctx := contextkey.WithClient(context.Background(), "invalid_client_type")
382 | 		request := mcpgo.CallToolRequest{
383 | 			Arguments: map[string]interface{}{
384 | 				"contact": "9876543210",
385 | 			},
386 | 		}
387 | 
388 | 		result, err := tool.GetHandler()(ctx, request)
389 | 
390 | 		if err != nil {
391 | 			t.Fatalf("Expected no error, got %v", err)
392 | 		}
393 | 
394 | 		if result == nil {
395 | 			t.Fatal("Expected result, got nil")
396 | 		}
397 | 
398 | 		if result.Text == "" {
399 | 			t.Fatal("Expected error message in result")
400 | 		}
401 | 
402 | 		expectedErrMsg := "invalid client type in context"
403 | 		if !strings.Contains(result.Text, expectedErrMsg) {
404 | 			t.Errorf(
405 | 				"Expected error message to contain '%s', got '%s'",
406 | 				expectedErrMsg,
407 | 				result.Text,
408 | 			)
409 | 		}
410 | 	})
411 | }
412 | 
413 | func Test_RevokeToken(t *testing.T) {
414 | 	// URL patterns for mocking
415 | 	revokeTokenPathFmt := fmt.Sprintf(
416 | 		"/%s/customers/%%s/tokens/%%s/cancel",
417 | 		constants.VERSION_V1,
418 | 	)
419 | 
420 | 	// Sample successful token revocation response
421 | 	successResp := map[string]interface{}{
422 | 		"deleted": true,
423 | 	}
424 | 
425 | 	// Error responses
426 | 	tokenNotFoundResp := map[string]interface{}{
427 | 		"error": map[string]interface{}{
428 | 			"code":        "BAD_REQUEST_ERROR",
429 | 			"description": "Token not found",
430 | 		},
431 | 	}
432 | 
433 | 	customerNotFoundResp := map[string]interface{}{
434 | 		"error": map[string]interface{}{
435 | 			"code":        "BAD_REQUEST_ERROR",
436 | 			"description": "Customer not found",
437 | 		},
438 | 	}
439 | 
440 | 	tests := []RazorpayToolTestCase{
441 | 		{
442 | 			Name: "successful token revocation with valid parameters",
443 | 			Request: map[string]interface{}{
444 | 				"customer_id": "cust_1Aa00000000003",
445 | 				"token_id":    "token_ABCDEFGH",
446 | 			},
447 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
448 | 				return mock.NewHTTPClient(
449 | 					mock.Endpoint{
450 | 						Path: fmt.Sprintf(
451 | 							revokeTokenPathFmt,
452 | 							"cust_1Aa00000000003",
453 | 							"token_ABCDEFGH",
454 | 						),
455 | 						Method:   "PUT",
456 | 						Response: successResp,
457 | 					},
458 | 				)
459 | 			},
460 | 			ExpectError:    false,
461 | 			ExpectedResult: successResp,
462 | 		},
463 | 		{
464 | 			Name: "token not found error",
465 | 			Request: map[string]interface{}{
466 | 				"customer_id": "cust_1Aa00000000003",
467 | 				"token_id":    "token_nonexistent",
468 | 			},
469 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
470 | 				return mock.NewHTTPClient(
471 | 					mock.Endpoint{
472 | 						Path: fmt.Sprintf(
473 | 							revokeTokenPathFmt,
474 | 							"cust_1Aa00000000003",
475 | 							"token_nonexistent",
476 | 						),
477 | 						Method:   "PUT",
478 | 						Response: tokenNotFoundResp,
479 | 					},
480 | 				)
481 | 			},
482 | 			ExpectError: true,
483 | 			ExpectedErrMsg: "Failed to revoke token token_nonexistent for " +
484 | 				"customer cust_1Aa00000000003: Token not found",
485 | 		},
486 | 		{
487 | 			Name: "customer not found error",
488 | 			Request: map[string]interface{}{
489 | 				"customer_id": "cust_nonexistent",
490 | 				"token_id":    "token_ABCDEFGH",
491 | 			},
492 | 			MockHttpClient: func() (*http.Client, *httptest.Server) {
493 | 				return mock.NewHTTPClient(
494 | 					mock.Endpoint{
495 | 						Path: fmt.Sprintf(
496 | 							revokeTokenPathFmt,
497 | 							"cust_nonexistent",
498 | 							"token_ABCDEFGH",
499 | 						),
500 | 						Method:   "PUT",
501 | 						Response: customerNotFoundResp,
502 | 					},
503 | 				)
504 | 			},
505 | 			ExpectError: true,
506 | 			ExpectedErrMsg: "Failed to revoke token token_ABCDEFGH for " +
507 | 				"customer cust_nonexistent: Customer not found",
508 | 		},
509 | 		{
510 | 			Name: "missing customer_id parameter",
511 | 			Request: map[string]interface{}{
512 | 				"token_id": "token_ABCDEFGH",
513 | 			},
514 | 			MockHttpClient: nil, // No HTTP client needed for validation error
515 | 			ExpectError:    true,
516 | 			ExpectedErrMsg: "missing required parameter: customer_id",
517 | 		},
518 | 		{
519 | 			Name: "missing token_id parameter",
520 | 			Request: map[string]interface{}{
521 | 				"customer_id": "cust_1Aa00000000003",
522 | 			},
523 | 			MockHttpClient: nil, // No HTTP client needed for validation error
524 | 			ExpectError:    true,
525 | 			ExpectedErrMsg: "missing required parameter: token_id",
526 | 		},
527 | 		{
528 | 			Name: "empty customer_id parameter",
529 | 			Request: map[string]interface{}{
530 | 				"customer_id": "",
531 | 				"token_id":    "token_ABCDEFGH",
532 | 			},
533 | 			MockHttpClient: nil, // No HTTP client needed for validation error
534 | 			ExpectError:    true,
535 | 			ExpectedErrMsg: "missing required parameter: customer_id",
536 | 		},
537 | 		{
538 | 			Name: "empty token_id parameter",
539 | 			Request: map[string]interface{}{
540 | 				"customer_id": "cust_1Aa00000000003",
541 | 				"token_id":    "",
542 | 			},
543 | 			MockHttpClient: nil, // No HTTP client needed for validation error
544 | 			ExpectError:    true,
545 | 			ExpectedErrMsg: "missing required parameter: token_id",
546 | 		},
547 | 		{
548 | 			Name: "null customer_id parameter",
549 | 			Request: map[string]interface{}{
550 | 				"customer_id": nil,
551 | 				"token_id":    "token_ABCDEFGH",
552 | 			},
553 | 			MockHttpClient: nil, // No HTTP client needed for validation error
554 | 			ExpectError:    true,
555 | 			ExpectedErrMsg: "missing required parameter: customer_id",
556 | 		},
557 | 		{
558 | 			Name: "null token_id parameter",
559 | 			Request: map[string]interface{}{
560 | 				"customer_id": "cust_1Aa00000000003",
561 | 				"token_id":    nil,
562 | 			},
563 | 			MockHttpClient: nil, // No HTTP client needed for validation error
564 | 			ExpectError:    true,
565 | 			ExpectedErrMsg: "missing required parameter: token_id",
566 | 		},
567 | 		{
568 | 			Name:    "both parameters missing",
569 | 			Request: map[string]interface{}{
570 | 				// No parameters
571 | 			},
572 | 			MockHttpClient: nil, // No HTTP client needed for validation error
573 | 			ExpectError:    true,
574 | 			ExpectedErrMsg: "missing required parameter: customer_id",
575 | 		},
576 | 	}
577 | 
578 | 	for _, tc := range tests {
579 | 		t.Run(tc.Name, func(t *testing.T) {
580 | 			runToolTest(t, tc, RevokeToken, "Revoke Token")
581 | 		})
582 | 	}
583 | }
584 | 
585 | // Test_RevokeToken_ClientContextScenarios tests scenarios
586 | // related to client context handling for 100% code coverage
587 | func Test_RevokeToken_ClientContextScenarios(t *testing.T) {
588 | 	obs := CreateTestObservability()
589 | 
590 | 	t.Run("no client in context and default is nil", func(t *testing.T) {
591 | 		// Create tool with nil client
592 | 		tool := RevokeToken(obs, nil)
593 | 
594 | 		// Create context without client
595 | 		ctx := context.Background()
596 | 		request := mcpgo.CallToolRequest{
597 | 			Arguments: map[string]interface{}{
598 | 				"customer_id": "cust_1Aa00000000003",
599 | 				"token_id":    "token_ABCDEFGH",
600 | 			},
601 | 		}
602 | 
603 | 		result, err := tool.GetHandler()(ctx, request)
604 | 
605 | 		if err != nil {
606 | 			t.Fatalf("Expected no error, got %v", err)
607 | 		}
608 | 
609 | 		if result == nil {
610 | 			t.Fatal("Expected result, got nil")
611 | 		}
612 | 
613 | 		if result.Text == "" {
614 | 			t.Fatal("Expected error message in result")
615 | 		}
616 | 
617 | 		expectedErrMsg := "no client found in context"
618 | 		if !strings.Contains(result.Text, expectedErrMsg) {
619 | 			t.Errorf(
620 | 				"Expected error message to contain '%s', got '%s'",
621 | 				expectedErrMsg,
622 | 				result.Text,
623 | 			)
624 | 		}
625 | 	})
626 | 
627 | 	t.Run("invalid client type in context", func(t *testing.T) {
628 | 		// Create tool with nil client
629 | 		tool := RevokeToken(obs, nil)
630 | 
631 | 		// Create context with invalid client type
632 | 		ctx := contextkey.WithClient(context.Background(), "invalid_client_type")
633 | 		request := mcpgo.CallToolRequest{
634 | 			Arguments: map[string]interface{}{
635 | 				"customer_id": "cust_1Aa00000000003",
636 | 				"token_id":    "token_ABCDEFGH",
637 | 			},
638 | 		}
639 | 
640 | 		result, err := tool.GetHandler()(ctx, request)
641 | 
642 | 		if err != nil {
643 | 			t.Fatalf("Expected no error, got %v", err)
644 | 		}
645 | 
646 | 		if result == nil {
647 | 			t.Fatal("Expected result, got nil")
648 | 		}
649 | 
650 | 		if result.Text == "" {
651 | 			t.Fatal("Expected error message in result")
652 | 		}
653 | 
654 | 		expectedErrMsg := "invalid client type in context"
655 | 		if !strings.Contains(result.Text, expectedErrMsg) {
656 | 			t.Errorf(
657 | 				"Expected error message to contain '%s', got '%s'",
658 | 				expectedErrMsg,
659 | 				result.Text,
660 | 			)
661 | 		}
662 | 	})
663 | }
664 | 
```

--------------------------------------------------------------------------------
/pkg/razorpay/payment_links_test.go:
--------------------------------------------------------------------------------

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

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

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