@payware/mcp-server 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +5 -0
  2. package/package.json +69 -66
  3. package/src/auth/sandbox-setup.js +186 -186
  4. package/src/isv/index.js +29 -1
  5. package/src/isv/invitations/index.js +471 -0
  6. package/src/isv/oauth2/obtain-token.js +19 -8
  7. package/src/isv/oauth2/token-helper.js +5 -2
  8. package/src/isv/poi/cancel-price.js +2 -10
  9. package/src/isv/poi/create.js +19 -0
  10. package/src/isv/poi/get-poi.js +2 -10
  11. package/src/isv/poi/get-qrcode.js +2 -10
  12. package/src/isv/poi/get-status.js +2 -10
  13. package/src/isv/poi/index.js +5 -0
  14. package/src/isv/poi/list-pois.js +2 -10
  15. package/src/isv/poi/set-price.js +217 -225
  16. package/src/isv/products/create-product.js +313 -313
  17. package/src/isv/products/update-product.js +264 -264
  18. package/src/isv/reference/index.js +249 -0
  19. package/src/isv/shops/index.js +22 -0
  20. package/src/isv/transactions/cancel-transaction.js +41 -10
  21. package/src/isv/transactions/create-transaction.js +20 -4
  22. package/src/isv/transactions/get-status.js +18 -11
  23. package/src/isv/transactions/process-transaction.js +160 -42
  24. package/src/isv/transactions/simulate-callback.js +128 -109
  25. package/src/isv/transactions/transaction-history.js +86 -90
  26. package/src/merchant/data/generate-report.js +19 -1
  27. package/src/merchant/index.js +21 -1
  28. package/src/merchant/logs/index.js +366 -0
  29. package/src/merchant/products/create-product.js +287 -287
  30. package/src/merchant/products/update-product.js +281 -281
  31. package/src/merchant/shops/index.js +18 -0
  32. package/src/merchant/transactions/cancel-transaction.js +10 -13
  33. package/src/merchant/transactions/create-transaction.js +88 -8
  34. package/src/merchant/transactions/get-status.js +40 -11
  35. package/src/merchant/transactions/process-transaction.js +2 -10
  36. package/src/merchant/transactions/simulate-callback.js +68 -9
  37. package/src/merchant/transactions/transaction-history.js +275 -257
  38. package/src/payment-institution/transactions/create-pi-transaction.js +2 -10
  39. package/src/payment-institution/transactions/finalize-transaction.js +333 -340
  40. package/src/payment-institution/transactions/get-transaction-history.js +2 -10
  41. package/src/payment-institution/transactions/get-transaction-status.js +38 -11
  42. package/src/payment-institution/transactions/process-pi-transaction.js +2 -10
  43. package/src/payment-institution/transactions/simulate-pi-callback.js +49 -9
  44. package/src/payment-institution/transactions/soundbite-transaction.js +2 -10
  45. package/src/shared/api-errors.js +188 -0
  46. package/src/shared/auth-headers.js +62 -0
  47. package/src/shared/data/data-api.js +11 -10
  48. package/src/shared/deep-links/api.js +5 -4
  49. package/src/shared/logs/logs-api.js +242 -0
  50. package/src/shared/poi/poi-create-api.js +419 -0
  51. package/src/shared/products/products-api.js +17 -16
  52. package/src/shared/shops/shops-api.js +155 -0
  53. package/src/tools/advanced-code-generator.js +2255 -2246
  54. package/src/utils/code-examples/common/auth-examples.js +1484 -1481
  55. package/src/utils/code-examples/common/helpers.js +7 -4
  56. package/src/utils/code-examples/isv/oauth2.js +458 -600
  57. package/src/utils/code-examples/isv/transactions.js +815 -812
  58. package/MCP-SETUP.md +0 -190
@@ -22,6 +22,11 @@ export async function createTransaction({
22
22
  partnerId,
23
23
  privateKey,
24
24
  passbackParams,
25
+ // POS terminal producer attribution. Optional, and independent of everything else on the request:
26
+ // it selects a revenue share out of payware's own fee and never changes what the merchant pays.
27
+ producerPartnerId,
28
+ terminalId,
29
+ terminalManufacturer,
25
30
  // QR Options
26
31
  qrFormat,
27
32
  qrBorder,
@@ -55,7 +60,31 @@ export async function createTransaction({
55
60
  if (!partnerId || !privateKey) {
56
61
  throw new Error('Partner ID and private key are required for proper JWT creation');
57
62
  }
58
-
63
+
64
+ // Refuse an empty producerPartnerId here rather than letting it reach the server.
65
+ // The server rejects an unrecognised producer id outright - a misconfigured terminal fails on its
66
+ // first sale, deliberately, so that an installer sees it - and an empty string is unrecognised.
67
+ // Sending one turns "no producer arrangement" into a failed transaction, when the correct way to
68
+ // say that is to omit the field entirely.
69
+ if (producerPartnerId !== undefined && String(producerPartnerId).trim() === '') {
70
+ throw new Error(
71
+ 'producerPartnerId must not be empty. Omit the field entirely when there is no POS terminal ' +
72
+ 'producer arrangement - an unrecognised value (including an empty string) is rejected by the ' +
73
+ 'server and fails the transaction.'
74
+ );
75
+ }
76
+
77
+ for (const [field, value] of [['terminalId', terminalId], ['terminalManufacturer', terminalManufacturer]]) {
78
+ if (value === undefined || value === null) continue;
79
+ if (String(value).length > 64) {
80
+ throw new Error(`${field} cannot exceed 64 characters`);
81
+ }
82
+ // The server rejects control characters in both fields; catching it here names the field.
83
+ if (/[\u0000-\u001F\u007F]/.test(String(value))) {
84
+ throw new Error(`${field} must not contain control characters`);
85
+ }
86
+ }
87
+
59
88
  // Build QR options if applicable
60
89
  const qrOptions = {};
61
90
  if (type === 'QR') {
@@ -82,9 +111,12 @@ export async function createTransaction({
82
111
  ...(friendlyName && { friendlyName }),
83
112
  ...(shop && { shop }),
84
113
  ...(callbackUrl && { callbackUrl }),
85
- ...(passbackParams && {
86
- passbackParams: typeof passbackParams === 'string' ? passbackParams : JSON.stringify(passbackParams)
114
+ ...(passbackParams && {
115
+ passbackParams: typeof passbackParams === 'string' ? passbackParams : JSON.stringify(passbackParams)
87
116
  }),
117
+ ...(producerPartnerId && { producerPartnerId }),
118
+ ...(terminalId && { terminalId }),
119
+ ...(terminalManufacturer && { terminalManufacturer }),
88
120
  trData: {
89
121
  amount: amount !== undefined ? amount.toString() : '0.00',
90
122
  currency,
@@ -133,7 +165,7 @@ export async function createTransaction({
133
165
  // Provide helpful error message for hash mismatches
134
166
  let enhancedMessage = error.response?.data?.message || error.message;
135
167
 
136
- if (error.response?.data?.code === 'ERR_INVALID_CONTENT_HASH' ||
168
+ if (error.response?.data?.errorCode === 'ERR_INVALID_CONTENT_HASH' ||
137
169
  enhancedMessage?.includes('SHA-256') ||
138
170
  enhancedMessage?.includes('contentSha256')) {
139
171
  enhancedMessage = `Hash Mismatch Error: The contentSha256 in JWT header doesn't match the request body.
@@ -149,9 +181,9 @@ Original error: ${enhancedMessage}`;
149
181
  error: {
150
182
  message: enhancedMessage,
151
183
  status: error.response?.status,
152
- code: error.response?.data?.code,
184
+ code: error.response?.data?.errorCode,
153
185
  details: error.response?.data,
154
- helpUrl: error.response?.data?.code === 'ERR_INVALID_CONTENT_HASH' ?
186
+ helpUrl: error.response?.data?.errorCode === 'ERR_INVALID_CONTENT_HASH' ?
155
187
  'https://github.com/payware/mcp-server#sha256-consistency' : undefined
156
188
  },
157
189
  timestamp: new Date().toISOString()
@@ -168,6 +200,7 @@ export const createTransactionTool = {
168
200
 
169
201
  📋 **ROOT LEVEL** (Transaction metadata):
170
202
  - shop, account, friendlyName, callbackUrl, passbackParams
203
+ - producerPartnerId, terminalId, terminalManufacturer (POS terminal producer attribution - see below)
171
204
 
172
205
  💰 **TRANSACTION DATA** (trData object):
173
206
  - amount, currency, reasonL1, reasonL2
@@ -204,7 +237,33 @@ export const createTransactionTool = {
204
237
  },
205
238
  "qrOptions": { ... }
206
239
  }
207
- \`\`\``,
240
+ \`\`\`
241
+
242
+ 🏭 **POS TERMINAL PRODUCER ATTRIBUTION** (optional, root level)
243
+
244
+ Only relevant if the transaction originates on a POS terminal whose producer has a revenue-share
245
+ agreement with payware. Three fields, all optional:
246
+
247
+ - \`producerPartnerId\` - the 8-character partnerId payware issued to the terminal producer.
248
+ **An unrecognised value fails the transaction with a 400** (\`ERR_INVALID_PRODUCER_ID\`) - it is
249
+ deliberately not ignored, so that a terminal configured with the wrong id fails on its first sale
250
+ where an installer can see it. **Omit the field entirely when there is no producer arrangement;
251
+ never send an empty string.**
252
+ - \`terminalId\` - the producer's own identifier for the physical terminal. Max 64 chars, no control
253
+ characters. payware records it and never interprets it: no format, no uniqueness enforcement, and
254
+ nothing about pricing or attribution depends on it.
255
+ - \`terminalManufacturer\` - device manufacturer as the terminal reports it. Max 64 chars, no control
256
+ characters. Recorded for provenance; a mismatch is surfaced in settlement review, never blocked.
257
+
258
+ Two things worth knowing:
259
+ 1. **It never changes what the merchant is charged.** The producer's share comes out of payware's own
260
+ fee, not out of a higher price for the merchant, whose fee always follows the fee hierarchy.
261
+ 2. **None of the three is returned.** They do not appear in the create response, in \`get_transaction_status\`,
262
+ in transaction history, or in any callback. What a producer earned is visible on their own
263
+ dashboard and settlements - attribution is recorded against the fee, not against the transaction.
264
+
265
+ If an ISV's software made the call, the ISV is attributed and the producer is not, even when the
266
+ ISV's software is running on that producer's hardware.`,
208
267
  inputSchema: {
209
268
  type: "object",
210
269
  properties: {
@@ -257,9 +316,23 @@ export const createTransactionTool = {
257
316
  },
258
317
  callbackUrl: {
259
318
  type: "string",
260
- description: "📋 ROOT LEVEL: HTTPS URL to receive transaction status callbacks (must be https://)",
319
+ description: "📋 ROOT LEVEL: HTTPS URL to receive transaction status callbacks. Must be https://, must present a publicly-trusted TLS certificate, and must resolve to a public address - internal/private addresses are rejected (ERR_UNRESOLVABLE_CALLBACK_URL).",
261
320
  format: "uri"
262
321
  },
322
+ producerPartnerId: {
323
+ type: "string",
324
+ description: "🏭 ROOT LEVEL: partnerId of the POS terminal producer whose terminal produced this transaction. Must be a partner registered with payware and flagged as a producer - an unrecognised value REJECTS the transaction (400 ERR_INVALID_PRODUCER_ID) rather than being ignored. Omit entirely when there is no producer arrangement; never send an empty string. Selects the producer's revenue share out of payware's own fee and never changes what the merchant is charged. Not returned in any response or callback."
325
+ },
326
+ terminalId: {
327
+ type: "string",
328
+ description: "🏭 ROOT LEVEL: Opaque identifier of the physical terminal, scoped to the producer's own estate. payware assumes no format and enforces no uniqueness; it is recorded for provenance and never affects attribution or pricing. Make it unique within your own estate anyway - that is what lets you answer 'which terminal produced this sale' later. Not returned in any response or callback.",
329
+ maxLength: 64
330
+ },
331
+ terminalManufacturer: {
332
+ type: "string",
333
+ description: "🏭 ROOT LEVEL: Device manufacturer as reported by the terminal. Recorded for provenance; a mismatch against the expected producer is surfaced in settlement review and never blocks the transaction. Not returned in any response or callback.",
334
+ maxLength: 64
335
+ },
263
336
  partnerId: {
264
337
  type: "string",
265
338
  description: "🔐 AUTHENTICATION: Partner ID from payware dashboard (not sent in API request). Uses PAYWARE_PARTNER_ID env var as default."
@@ -355,6 +428,10 @@ export const createTransactionTool = {
355
428
  partnerId = getPartnerIdSafe(),
356
429
  privateKey = getPrivateKeySafe(args.useSandbox ?? true),
357
430
  passbackParams,
431
+ // POS terminal producer attribution
432
+ producerPartnerId,
433
+ terminalId,
434
+ terminalManufacturer,
358
435
  // QR Options
359
436
  qrFormat,
360
437
  qrBorder,
@@ -396,6 +473,9 @@ export const createTransactionTool = {
396
473
  partnerId,
397
474
  privateKey,
398
475
  passbackParams,
476
+ producerPartnerId,
477
+ terminalId,
478
+ terminalManufacturer,
399
479
  // QR Options
400
480
  qrFormat,
401
481
  qrBorder,
@@ -1,6 +1,7 @@
1
1
  import axios from 'axios';
2
2
  import { createJWTToken } from '../../core/auth/jwt-token.js';
3
3
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
4
+ import { apiErrorResult } from '../../shared/api-errors.js';
4
5
 
5
6
  /**
6
7
  * Get transaction status from payware API
@@ -37,16 +38,7 @@ export async function getTransactionStatus({ transactionId, partnerId, privateKe
37
38
  timestamp: new Date().toISOString()
38
39
  };
39
40
  } catch (error) {
40
- return {
41
- success: false,
42
- error: {
43
- message: error.response?.data?.message || error.message,
44
- status: error.response?.status,
45
- code: error.response?.data?.code,
46
- details: error.response?.data
47
- },
48
- timestamp: new Date().toISOString()
49
- };
41
+ return apiErrorResult(error);
50
42
  }
51
43
  }
52
44
 
@@ -79,7 +71,44 @@ export const getTransactionStatusTool = {
79
71
 
80
72
  **Endpoint:** GET /transactions/{transactionId}
81
73
  **Use Case:** Check status of ACTIVE transactions only. This endpoint only returns transactions with ACTIVE status.
82
- **Note:** For transactions with final statuses (CONFIRMED, DECLINED, FAILED, EXPIRED, CANCELLED), use 'payware_transactions_get_history' instead.`,
74
+ **Note:** For transactions with final statuses (CONFIRMED, DECLINED, FAILED, EXPIRED, CANCELLED), use 'payware_transactions_get_history' instead.
75
+
76
+ 🛍️ **Product-originated transactions carry a nested \`product\` object.** Since 2026-07-22 the
77
+ presentation fields are grouped under \`product\` rather than sitting flat on the response - the old
78
+ top-level \`imageUrl\` is gone. Discriminate a product-originated transaction by the **presence of the
79
+ object**, not by probing individual fields:
80
+
81
+ \`\`\`json
82
+ { "transactionId": "pw7e4rCToG", "amount": "57.60", "currency": "EUR",
83
+ "product": { "productId": "pr7e4rCToG", "type": "ITEM", "name": "Cotton T-shirt, black",
84
+ "shortDescription": "...", "longDescription": "...",
85
+ "imageUrl": "https://...", "imageDigest": "9f86d081...", "imageContentType": "image/png",
86
+ "imageBytes": 20480, "termsUrl": "https://...", "termsText": "...",
87
+ "regularPrice": "63.36", "shippable": false, "quickPay": true, "shopName": "My shop" } }
88
+ \`\`\`
89
+
90
+ Three things about that object:
91
+
92
+ - **\`imageDigest\` is an obligation, not decoration.** It is the SHA-256 payware pinned for the image
93
+ at \`imageUrl\`. Fetch the image, verify its bytes against this digest before rendering it to a
94
+ payer, and fall back to a neutral placeholder on mismatch. Present only when \`imageUrl\` is the
95
+ product's own image.
96
+ - **\`regularPrice\` is display-only, and appears only when there is a genuine saving** - i.e. when it
97
+ is higher than the transaction \`amount\`. The top-level \`amount\` and \`currency\` are authoritative
98
+ and are what the payer pays; the object does not repeat them.
99
+ - **Availability follows the merchant's plan.** \`imageUrl\` and \`termsUrl\` are Standard and above;
100
+ \`termsText\` and the descriptions are universal. A missing field is a plan boundary, not an error.
101
+
102
+ For a \`pr\`/\`ps\` product identifier the values are resolved live; for the resulting \`pw\` transaction
103
+ they are the snapshot frozen at creation, so the response always reflects what the payer could have
104
+ been shown at scan time.
105
+
106
+ 💰 **Amounts follow the currency, not a fixed two decimals.** \`amount\`, \`fee\`, \`payerAmount\` and
107
+ \`payeeAmount\` are decimal strings at the smallest unit their own currency can be paid in - none for
108
+ JPY, two for EUR, eight for BTC. Parse them with a decimal type; never compare them as strings, and
109
+ never assume two places. \`feeRate\` carries up to six decimals and \`feeFixed\` full stored precision:
110
+ both are informational, and recomputing the fee from them and submitting that at finalize is rejected
111
+ with ERR_FEE_MISMATCH. Echo \`fee\` back unchanged instead.`,
83
112
  inputSchema: {
84
113
  type: "object",
85
114
  properties: {
@@ -2,6 +2,7 @@ import axios from 'axios';
2
2
  import { createJWTToken } from '../../core/auth/jwt-token.js';
3
3
  import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
4
4
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
5
+ import { apiErrorResult } from '../../shared/api-errors.js';
5
6
 
6
7
  /**
7
8
  * Process a transaction via payware API (for merchants accepting payments)
@@ -97,16 +98,7 @@ export async function processTransaction({
97
98
  timestamp: new Date().toISOString()
98
99
  };
99
100
  } catch (error) {
100
- return {
101
- success: false,
102
- error: {
103
- message: error.response?.data?.message || error.message,
104
- status: error.response?.status,
105
- code: error.response?.data?.errorCode,
106
- details: error.response?.data
107
- },
108
- timestamp: new Date().toISOString()
109
- };
101
+ return apiErrorResult(error);
110
102
  }
111
103
  }
112
104
 
@@ -17,10 +17,18 @@ export function generateMockCallback(transactionId, status = 'CONFIRMED', option
17
17
  payeeAmount = null,
18
18
  paymentMethod = null,
19
19
  statusMessage = null,
20
- passbackParams = null
20
+ passbackParams = null,
21
+ // The fee breakdown the server attaches whenever the transaction has a fee configuration.
22
+ // Before 2026-08-21 a merchant's FINALIZED callback carried the total fee but not the two
23
+ // numbers that produced it, so reconciling a charge meant asking payware. It carries both now.
24
+ feeFixed = '0.1000',
25
+ feeRate = '0.0150',
26
+ // Pass fee: null to model the anomaly the server reports by OMITTING the field - a transaction
27
+ // with no recorded fee. That is deliberately distinguishable from a genuine "0.00".
28
+ fee: feeOverride = undefined
21
29
  } = options;
22
30
 
23
- // Calculate fee like Java implementation: |payerAmount - amount| or |payeeAmount - amount|
31
+ // Calculate fee like the server: |payerAmount - amount| or |payeeAmount - amount|.
24
32
  const transactionAmount = parseFloat(amount);
25
33
  let fee = '0.00';
26
34
  if (payerAmount !== null) {
@@ -28,19 +36,29 @@ export function generateMockCallback(transactionId, status = 'CONFIRMED', option
28
36
  } else if (payeeAmount !== null) {
29
37
  fee = Math.abs(parseFloat(payeeAmount) - transactionAmount).toFixed(2);
30
38
  }
39
+ if (feeOverride !== undefined) {
40
+ fee = feeOverride;
41
+ }
31
42
 
32
43
  const currentTime = Date.now();
44
+ // Built with the same omission rule the server applies. TransactionCallbackPayload is
45
+ // @JsonInclude(NON_NULL), so a property with no value is ABSENT from the JSON rather than sent as
46
+ // null or 0 - and a consumer is entitled to tell those apart. Emitting "passbackParams": null or
47
+ // "fee": "0.0000" here would have taught an integrator to expect a shape the server never sends.
33
48
  const basePayload = {
34
49
  callbackType: 'TRANSACTION_FINALIZED',
35
50
  transactionId,
36
- passbackParams,
51
+ ...(passbackParams !== null && passbackParams !== undefined && { passbackParams }),
37
52
  amount,
38
- fee,
53
+ ...(fee !== null && fee !== undefined && { fee }),
54
+ ...(feeFixed !== null && feeFixed !== undefined && { feeFixed }),
55
+ ...(feeRate !== null && feeRate !== undefined && { feeRate }),
39
56
  currency,
40
57
  status,
41
- statusMessage,
58
+ ...(statusMessage !== null && statusMessage !== undefined && { statusMessage }),
42
59
  ...(paymentMethod && { paymentMethod }),
43
60
  created: currentTime - 300000, // Created 5 minutes ago (milliseconds)
61
+ // Absent on EXPIRED: the expiry sweep transitions status in bulk and sets no finalized moment.
44
62
  ...(status !== 'EXPIRED' && { finalized: currentTime })
45
63
  };
46
64
 
@@ -116,7 +134,32 @@ export async function simulateCallbackDelivery(callbackUrl, payload) {
116
134
  */
117
135
  export const simulateCallbackTool = {
118
136
  name: "payware_operations_simulate_callback",
119
- description: "Simulate callback scenarios for testing webhook handling",
137
+ description: `Simulate callback scenarios for testing webhook handling.
138
+
139
+ Generates the payload a merchant's \`callbackUrl\` actually receives, so a handler can be built and
140
+ tested against the real shape. Four properties of that shape are easy to get wrong:
141
+
142
+ **1. Absent means absent, not zero.** The payload omits properties with no value rather than sending
143
+ \`null\` or \`0\`. \`statusMessage\`, \`passbackParams\`, \`fee\`, \`feeFixed\` and \`feeRate\` are all simply
144
+ missing when they do not apply. A missing \`fee\` means the fee is *unknown* for that transaction - it
145
+ does not mean the transaction was free. Do not write a handler that reads \`payload.fee\` as a number
146
+ without checking it is there.
147
+
148
+ **2. \`finalized\` is absent on EXPIRED.** An expired transaction is closed by the platform's expiry
149
+ sweep, which sets no finalization moment. Every other status carries one.
150
+
151
+ **3. \`EXPIRED\` is a non-payment, not a failure.** The payer never paid within \`timeToLive\`. Release
152
+ the order or the stock, as you would for a cancellation - do not treat it as an error to retry.
153
+
154
+ **4. Deduplicate on \`(transactionId, callbackType)\`.** payware retries a callback once a second, up
155
+ to 15 times, until the endpoint answers 200. That pair is stable across every retry of the same
156
+ event, and it is the key payware guarantees. A handler that does not deduplicate will eventually
157
+ process one payment twice.
158
+
159
+ **Not on this payload:** \`deliveryAddress\` (removed 2026-08-07 - fetch it from
160
+ \`payware_operations_get_transaction_history\`, which is authenticated and tenant-scoped),
161
+ \`producerPartnerId\`, \`transactionType\` and \`initiatedBy\` (the last two are carried on
162
+ TRANSACTION_PROCESSED, which merchants do not receive).`,
120
163
  inputSchema: {
121
164
  type: "object",
122
165
  properties: {
@@ -148,8 +191,17 @@ export const simulateCallbackTool = {
148
191
  },
149
192
  fee: {
150
193
  type: "string",
151
- description: "Fee amount charged by payment institution (e.g., '0.00' for no fee)",
152
- default: "0.00"
194
+ description: "The payware fee on this transaction, at the currency's smallest payable unit (e.g. '0.29'). Not always two decimals - it follows the currency. Pass null to model the anomaly case, where a transaction has no recorded fee and the server OMITS the property entirely; that is deliberately distinguishable from a genuine '0.00'."
195
+ },
196
+ feeFixed: {
197
+ type: "string",
198
+ description: "Fixed fee component of the applicable fee configuration, e.g. '0.1000'. Informational, for reconciliation. Present on FINALIZED callbacks since 2026-08-21 - before that a merchant got the total fee but not the breakdown that produced it. Pass null to model a transaction with no fee configuration, where it is omitted.",
199
+ default: "0.1000"
200
+ },
201
+ feeRate: {
202
+ type: "string",
203
+ description: "Variable rate of the applicable fee configuration, e.g. '0.0150' = 1.5%. Informational, same as feeFixed. Pass null to model a transaction with no fee configuration.",
204
+ default: "0.0150"
153
205
  },
154
206
  statusMessage: {
155
207
  type: "string",
@@ -172,7 +224,12 @@ export const simulateCallbackTool = {
172
224
  callbackUrl,
173
225
  amount = '57.60',
174
226
  currency = 'EUR',
175
- fee = '0.00',
227
+ // No default: an omitted fee falls through to generateMockCallback's own derivation.
228
+ // Defaulting it to '0.00' here made every simulated callback assert a zero fee, which is the
229
+ // one value the real payload never sends for "no fee" - it omits the property instead.
230
+ fee,
231
+ feeFixed = '0.1000',
232
+ feeRate = '0.0150',
176
233
  statusMessage,
177
234
  paymentMethod
178
235
  } = args;
@@ -186,6 +243,8 @@ export const simulateCallbackTool = {
186
243
  amount,
187
244
  currency,
188
245
  fee,
246
+ feeFixed,
247
+ feeRate,
189
248
  statusMessage,
190
249
  paymentMethod
191
250
  });