@payware/mcp-server 1.3.2 → 1.4.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 (93) hide show
  1. package/.env.example +4 -0
  2. package/README.md +18 -0
  3. package/package.json +7 -9
  4. package/src/auth/rsa-keys.js +12 -6
  5. package/src/auth/sandbox-setup.js +2 -0
  6. package/src/config/toolsets.js +104 -0
  7. package/src/core/auth/jwt-factory.js +4 -1
  8. package/src/core/auth/jwt-token.js +4 -0
  9. package/src/index.js +10 -6
  10. package/src/isv/data/index.js +15 -14
  11. package/src/isv/deep-links/delete-product-link.js +3 -2
  12. package/src/isv/deep-links/delete-transaction-link.js +3 -2
  13. package/src/isv/deep-links/get-product-link.js +8 -6
  14. package/src/isv/deep-links/get-transaction-link.js +8 -6
  15. package/src/isv/index.js +6 -0
  16. package/src/isv/invitations/index.js +5 -0
  17. package/src/isv/oauth2/get-token-info.js +10 -5
  18. package/src/isv/oauth2/obtain-token.js +9 -4
  19. package/src/isv/oauth2/token-helper.js +28 -26
  20. package/src/isv/poi/cancel-price.js +9 -2
  21. package/src/isv/poi/get-poi.js +4 -3
  22. package/src/isv/poi/get-qrcode.js +40 -9
  23. package/src/isv/poi/get-status.js +196 -168
  24. package/src/isv/poi/list-pois.js +6 -6
  25. package/src/isv/poi/set-price.js +40 -53
  26. package/src/isv/products/audios.js +8 -2
  27. package/src/isv/products/create-product.js +2 -0
  28. package/src/isv/products/delete-product.js +2 -0
  29. package/src/isv/products/get-product-image.js +2 -0
  30. package/src/isv/products/get-product.js +2 -0
  31. package/src/isv/products/list-products.js +2 -0
  32. package/src/isv/products/schedules.js +6 -0
  33. package/src/isv/products/update-product.js +2 -0
  34. package/src/isv/reference/index.js +2 -0
  35. package/src/isv/transactions/cancel-transaction.js +2 -0
  36. package/src/isv/transactions/create-transaction.js +2 -0
  37. package/src/isv/transactions/get-status.js +3 -1
  38. package/src/isv/transactions/process-transaction.js +2 -0
  39. package/src/isv/transactions/simulate-callback.js +21 -10
  40. package/src/isv/transactions/transaction-history.js +2 -0
  41. package/src/merchant/data/export-report.js +2 -0
  42. package/src/merchant/data/generate-report.js +2 -0
  43. package/src/merchant/data/get-export-list.js +2 -0
  44. package/src/merchant/data/get-report-requests.js +2 -0
  45. package/src/merchant/data/get-report-status.js +2 -0
  46. package/src/merchant/data/index.js +15 -10
  47. package/src/merchant/data/list-reports.js +2 -0
  48. package/src/merchant/deep-links/delete-product-link.js +3 -2
  49. package/src/merchant/deep-links/delete-transaction-link.js +3 -2
  50. package/src/merchant/deep-links/get-product-link.js +8 -6
  51. package/src/merchant/deep-links/get-transaction-link.js +8 -6
  52. package/src/merchant/index.js +7 -1
  53. package/src/merchant/logs/index.js +4 -0
  54. package/src/merchant/products/audios.js +15 -14
  55. package/src/merchant/products/create-product.js +3 -2
  56. package/src/merchant/products/delete-product.js +3 -2
  57. package/src/merchant/products/get-product-image.js +3 -2
  58. package/src/merchant/products/get-product.js +3 -2
  59. package/src/merchant/products/list-products.js +3 -2
  60. package/src/merchant/products/schedules.js +11 -10
  61. package/src/merchant/products/update-product.js +3 -2
  62. package/src/merchant/transactions/cancel-transaction.js +3 -2
  63. package/src/merchant/transactions/create-transaction.js +3 -1
  64. package/src/merchant/transactions/get-status.js +4 -3
  65. package/src/merchant/transactions/process-transaction.js +5 -4
  66. package/src/merchant/transactions/simulate-callback.js +325 -313
  67. package/src/merchant/transactions/transaction-history.js +7 -6
  68. package/src/payment-institution/data/index.js +15 -14
  69. package/src/payment-institution/deep-links/delete-transaction-link.js +3 -2
  70. package/src/payment-institution/deep-links/get-transaction-link.js +8 -6
  71. package/src/payment-institution/transactions/create-pi-transaction.js +4 -3
  72. package/src/payment-institution/transactions/finalize-transaction.js +6 -3
  73. package/src/payment-institution/transactions/get-transaction-history.js +8 -5
  74. package/src/payment-institution/transactions/get-transaction-status.js +16 -13
  75. package/src/payment-institution/transactions/process-pi-transaction.js +7 -6
  76. package/src/payment-institution/transactions/simulate-pi-callback.js +388 -375
  77. package/src/payment-institution/transactions/soundbite-transaction.js +6 -5
  78. package/src/proxy-server.js +25 -0
  79. package/src/shared/placements/api.js +51 -0
  80. package/src/shared/placements/tools.js +519 -0
  81. package/src/shared/poi/poi-create-api.js +4 -1
  82. package/src/shared/products/products-api.js +36 -25
  83. package/src/shared/shops/shops-api.js +2 -0
  84. package/src/shared/tool-annotations.js +53 -0
  85. package/src/tools/advanced-code-generator.js +17 -265
  86. package/src/utils/code-examples/common/auth-examples.js +1 -1
  87. package/src/utils/code-examples/isv/products.js +1 -1
  88. package/src/utils/code-examples/isv/transactions.js +1 -1
  89. package/src/utils/code-examples/merchant/transactions.js +1 -1
  90. package/src/utils/format-request.js +2 -0
  91. package/src/utils/json-formatter.js +2 -0
  92. package/src/utils/server-info.js +2 -0
  93. package/src/utils/code-examples.js +0 -646
@@ -1,314 +1,326 @@
1
- /**
2
- * Simulate callback scenarios for testing
3
- */
4
-
5
- /**
6
- * Generate mock callback payload
7
- * @param {string} transactionId - Transaction ID
8
- * @param {string} status - Transaction status (CONFIRMED, DECLINED, FAILED, EXPIRED, CANCELLED)
9
- * @param {Object} options - Additional options
10
- * @returns {Object} Mock callback payload
11
- */
12
- export function generateMockCallback(transactionId, status = 'CONFIRMED', options = {}) {
13
- const {
14
- amount = '57.60',
15
- currency = 'EUR',
16
- payerAmount = null,
17
- payeeAmount = null,
18
- paymentMethod = null,
19
- statusMessage = 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
29
- } = options;
30
-
31
- // Calculate fee like the server: |payerAmount - amount| or |payeeAmount - amount|.
32
- const transactionAmount = parseFloat(amount);
33
- let fee = '0.00';
34
- if (payerAmount !== null) {
35
- fee = Math.abs(parseFloat(payerAmount) - transactionAmount).toFixed(2);
36
- } else if (payeeAmount !== null) {
37
- fee = Math.abs(parseFloat(payeeAmount) - transactionAmount).toFixed(2);
38
- }
39
- if (feeOverride !== undefined) {
40
- fee = feeOverride;
41
- }
42
-
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.
48
- const basePayload = {
49
- callbackType: 'TRANSACTION_FINALIZED',
50
- transactionId,
51
- ...(passbackParams !== null && passbackParams !== undefined && { passbackParams }),
52
- amount,
53
- ...(fee !== null && fee !== undefined && { fee }),
54
- ...(feeFixed !== null && feeFixed !== undefined && { feeFixed }),
55
- ...(feeRate !== null && feeRate !== undefined && { feeRate }),
56
- currency,
57
- status,
58
- ...(statusMessage !== null && statusMessage !== undefined && { statusMessage }),
59
- ...(paymentMethod && { paymentMethod }),
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.
62
- ...(status !== 'EXPIRED' && { finalized: currentTime })
63
- };
64
-
65
- // Add status-specific statusMessage
66
- switch (status) {
67
- case 'CONFIRMED':
68
- return {
69
- ...basePayload,
70
- statusMessage: statusMessage || 'Transaction confirmed'
71
- };
72
-
73
- case 'DECLINED':
74
- return {
75
- ...basePayload,
76
- statusMessage: statusMessage || 'Insufficient funds'
77
- };
78
-
79
- case 'FAILED':
80
- return {
81
- ...basePayload,
82
- statusMessage: statusMessage || 'Payment processing failed'
83
- };
84
-
85
- case 'EXPIRED':
86
- return {
87
- ...basePayload,
88
- statusMessage: statusMessage || 'Transaction expired',
89
- finalized: undefined // Remove finalized for expired transactions
90
- };
91
-
92
- case 'CANCELLED':
93
- return {
94
- ...basePayload,
95
- statusMessage: statusMessage || 'Transaction cancelled'
96
- };
97
-
98
- default:
99
- return basePayload;
100
- }
101
- }
102
-
103
- /**
104
- * Simulate callback delivery
105
- * @param {string} callbackUrl - URL to send callback to
106
- * @param {Object} payload - Callback payload
107
- * @returns {Object} Simulation result
108
- */
109
- export async function simulateCallbackDelivery(callbackUrl, payload) {
110
- if (!callbackUrl || !callbackUrl.startsWith('http')) {
111
- return {
112
- success: false,
113
- error: 'Invalid callback URL provided'
114
- };
115
- }
116
-
117
- // In a real implementation, this would actually send HTTP POST
118
- // For simulation, we just return what would be sent
119
- return {
120
- success: true,
121
- url: callbackUrl,
122
- payload,
123
- headers: {
124
- 'Content-Type': 'application/json',
125
- 'Authorization': 'Bearer mock-jwt-token-' + Date.now()
126
- },
127
- simulatedAt: new Date().toISOString(),
128
- note: 'This is a simulation - no actual HTTP request was made. Real payware callbacks use JWT tokens in Authorization header.'
129
- };
130
- }
131
-
132
- /**
133
- * Simulate callback tool implementation
134
- */
135
- export const simulateCallbackTool = {
136
- name: "payware_operations_simulate_callback",
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).`,
163
- inputSchema: {
164
- type: "object",
165
- properties: {
166
- transactionId: {
167
- type: "string",
168
- description: "Transaction ID to simulate callback for"
169
- },
170
- status: {
171
- type: "string",
172
- enum: ["CONFIRMED", "DECLINED", "FAILED", "EXPIRED", "CANCELLED"],
173
- description: "Transaction status to simulate",
174
- default: "CONFIRMED"
175
- },
176
- callbackUrl: {
177
- type: "string",
178
- description: "URL where callback would be sent (for simulation only)",
179
- format: "uri"
180
- },
181
- amount: {
182
- type: "string",
183
- description: "Transaction amount (e.g., '57.60')",
184
- default: "57.60"
185
- },
186
- currency: {
187
- type: "string",
188
- enum: ["EUR", "USD", "GBP"],
189
- description: "Transaction currency",
190
- default: "EUR"
191
- },
192
- fee: {
193
- type: "string",
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"
205
- },
206
- statusMessage: {
207
- type: "string",
208
- description: "Custom status message for the transaction callback"
209
- },
210
- paymentMethod: {
211
- type: "string",
212
- enum: ["A2A", "CARD_FUNDED", "BNPL", "INSTANT_CREDIT"],
213
- description: "Payment method chosen by customer. A2A = direct transfer. CARD_FUNDED = card-linked account. BNPL = buy now pay later. INSTANT_CREDIT = credit line."
214
- }
215
- },
216
- required: ["transactionId"],
217
- additionalProperties: false
218
- },
219
-
220
- async handler(args) {
221
- const {
222
- transactionId,
223
- status = 'CONFIRMED',
224
- callbackUrl,
225
- amount = '57.60',
226
- currency = 'EUR',
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',
233
- statusMessage,
234
- paymentMethod
235
- } = args;
236
-
237
- if (!transactionId) {
238
- throw new Error("Transaction ID is required");
239
- }
240
-
241
- // Generate mock callback payload
242
- const payload = generateMockCallback(transactionId, status, {
243
- amount,
244
- currency,
245
- fee,
246
- feeFixed,
247
- feeRate,
248
- statusMessage,
249
- paymentMethod
250
- });
251
-
252
- // Simulate callback delivery if URL provided
253
- let deliveryResult = null;
254
- if (callbackUrl) {
255
- deliveryResult = await simulateCallbackDelivery(callbackUrl, payload);
256
- }
257
-
258
- const statusEmojis = {
259
- 'CONFIRMED': '✅',
260
- 'DECLINED': '❌',
261
- 'FAILED': '💥',
262
- 'EXPIRED': '⏰',
263
- 'CANCELLED': '🚫'
264
- };
265
-
266
- const statusEmoji = statusEmojis[status] || '❓';
267
-
268
- return {
269
- content: [{
270
- type: "text",
271
- text: `🔄 **Callback Simulation Complete**
272
-
273
- **Transaction:** ${transactionId}
274
- **Simulated Status:** ${statusEmoji} ${status}
275
-
276
- **Mock Callback Payload:**
277
- \`\`\`json
278
- ${JSON.stringify(payload, null, 2)}
279
- \`\`\`
280
-
281
- ${deliveryResult ? `
282
- **Callback Delivery Simulation:**
283
- ${deliveryResult.success ? '✅ Callback would be delivered successfully' : '❌ Callback delivery would fail'}
284
-
285
- **Delivery Details:**
286
- - URL: ${deliveryResult.url || 'N/A'}
287
- - Headers: ${JSON.stringify(deliveryResult.headers || {}, null, 2)}
288
- - Simulated At: ${deliveryResult.simulatedAt || 'N/A'}
289
-
290
- **Note:** ${deliveryResult.note || 'No actual HTTP request was made'}
291
- ` : '**No callback URL provided** - only payload generated'}
292
-
293
- **Status Explanations:**
294
- - ✅ **CONFIRMED**: Successfully finalized
295
- - ❌ **DECLINED**: Declined by the user, processing or finalizing payment institutions
296
- - 💥 **FAILED**: Failed due to technical reasons or other
297
- - ⏰ **EXPIRED**: Time to live of the transaction has passed
298
- - 🚫 **CANCELLED**: Transaction canceled by the originator
299
-
300
- **Integration Testing:**
301
- 1. Use this payload to test your callback handler
302
- 2. Verify your system processes each status correctly
303
- 3. Check error handling for DECLINED/FAILED statuses
304
- 4. Ensure proper logging and monitoring
305
-
306
- **Next Steps:**
307
- 1. Implement callback endpoint to receive this payload
308
- 2. Test with different status scenarios
309
- 3. Add signature verification in production
310
- 4. Set up proper error handling and retry logic`
311
- }]
312
- };
313
- }
1
+ /**
2
+ * Simulate callback scenarios for testing
3
+ */
4
+
5
+ /**
6
+ * Generate mock callback payload
7
+ * @param {string} transactionId - Transaction ID
8
+ * @param {string} status - Transaction status (CONFIRMED, DECLINED, FAILED, EXPIRED, CANCELLED)
9
+ * @param {Object} options - Additional options
10
+ * @returns {Object} Mock callback payload
11
+ */
12
+ import { LOCAL } from '../../shared/tool-annotations.js';
13
+
14
+ export function generateMockCallback(transactionId, status = 'CONFIRMED', options = {}) {
15
+ const {
16
+ amount = '57.60',
17
+ currency = 'EUR',
18
+ payerAmount = null,
19
+ payeeAmount = null,
20
+ paymentMethod = null,
21
+ statusMessage = null,
22
+ passbackParams = null,
23
+ // The POI that created the transaction, on a POI sale's callbacks only (server S8, 2026-09-28).
24
+ poiId = null,
25
+ // The fee breakdown the server attaches whenever the transaction has a fee configuration.
26
+ // Before 2026-08-21 a merchant's FINALIZED callback carried the total fee but not the two
27
+ // numbers that produced it, so reconciling a charge meant asking payware. It carries both now.
28
+ feeFixed = '0.1000',
29
+ feeRate = '0.0150',
30
+ // Pass fee: null to model the anomaly the server reports by OMITTING the field - a transaction
31
+ // with no recorded fee. That is deliberately distinguishable from a genuine "0.00".
32
+ fee: feeOverride = undefined
33
+ } = options;
34
+
35
+ // Calculate fee like the server: |payerAmount - amount| or |payeeAmount - amount|.
36
+ const transactionAmount = parseFloat(amount);
37
+ let fee = '0.00';
38
+ if (payerAmount !== null) {
39
+ fee = Math.abs(parseFloat(payerAmount) - transactionAmount).toFixed(2);
40
+ } else if (payeeAmount !== null) {
41
+ fee = Math.abs(parseFloat(payeeAmount) - transactionAmount).toFixed(2);
42
+ }
43
+ if (feeOverride !== undefined) {
44
+ fee = feeOverride;
45
+ }
46
+
47
+ const currentTime = Date.now();
48
+ // Built with the same omission rule the server applies. TransactionCallbackPayload is
49
+ // @JsonInclude(NON_NULL), so a property with no value is ABSENT from the JSON rather than sent as
50
+ // null or 0 - and a consumer is entitled to tell those apart. Emitting "passbackParams": null or
51
+ // "fee": "0.0000" here would have taught an integrator to expect a shape the server never sends.
52
+ const basePayload = {
53
+ callbackType: 'TRANSACTION_FINALIZED',
54
+ transactionId,
55
+ ...(poiId && { poiId }),
56
+ ...(passbackParams !== null && passbackParams !== undefined && { passbackParams }),
57
+ amount,
58
+ ...(fee !== null && fee !== undefined && { fee }),
59
+ ...(feeFixed !== null && feeFixed !== undefined && { feeFixed }),
60
+ ...(feeRate !== null && feeRate !== undefined && { feeRate }),
61
+ currency,
62
+ status,
63
+ ...(statusMessage !== null && statusMessage !== undefined && { statusMessage }),
64
+ ...(paymentMethod && { paymentMethod }),
65
+ created: currentTime - 300000, // Created 5 minutes ago (milliseconds)
66
+ // Absent on EXPIRED: the expiry sweep transitions status in bulk and sets no finalized moment.
67
+ ...(status !== 'EXPIRED' && { finalized: currentTime })
68
+ };
69
+
70
+ // Add status-specific statusMessage
71
+ switch (status) {
72
+ case 'CONFIRMED':
73
+ return {
74
+ ...basePayload,
75
+ statusMessage: statusMessage || 'Transaction confirmed'
76
+ };
77
+
78
+ case 'DECLINED':
79
+ return {
80
+ ...basePayload,
81
+ statusMessage: statusMessage || 'Insufficient funds'
82
+ };
83
+
84
+ case 'FAILED':
85
+ return {
86
+ ...basePayload,
87
+ statusMessage: statusMessage || 'Payment processing failed'
88
+ };
89
+
90
+ case 'EXPIRED':
91
+ return {
92
+ ...basePayload,
93
+ statusMessage: statusMessage || 'Transaction expired',
94
+ finalized: undefined // Remove finalized for expired transactions
95
+ };
96
+
97
+ case 'CANCELLED':
98
+ return {
99
+ ...basePayload,
100
+ statusMessage: statusMessage || 'Transaction cancelled'
101
+ };
102
+
103
+ default:
104
+ return basePayload;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Simulate callback delivery
110
+ * @param {string} callbackUrl - URL to send callback to
111
+ * @param {Object} payload - Callback payload
112
+ * @returns {Object} Simulation result
113
+ */
114
+ export async function simulateCallbackDelivery(callbackUrl, payload) {
115
+ if (!callbackUrl || !callbackUrl.startsWith('http')) {
116
+ return {
117
+ success: false,
118
+ error: 'Invalid callback URL provided'
119
+ };
120
+ }
121
+
122
+ // In a real implementation, this would actually send HTTP POST
123
+ // For simulation, we just return what would be sent
124
+ return {
125
+ success: true,
126
+ url: callbackUrl,
127
+ payload,
128
+ headers: {
129
+ 'Content-Type': 'application/json',
130
+ 'Authorization': 'Bearer mock-jwt-token-' + Date.now()
131
+ },
132
+ simulatedAt: new Date().toISOString(),
133
+ note: 'This is a simulation - no actual HTTP request was made. Real payware callbacks use JWT tokens in Authorization header.'
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Simulate callback tool implementation
139
+ */
140
+ export const simulateCallbackTool = {
141
+ name: "payware_operations_simulate_callback",
142
+ annotations: LOCAL,
143
+ description: `Simulate callback scenarios for testing webhook handling.
144
+
145
+ Generates the payload a merchant's \`callbackUrl\` actually receives, so a handler can be built and
146
+ tested against the real shape. Four properties of that shape are easy to get wrong:
147
+
148
+ **1. Absent means absent, not zero.** The payload omits properties with no value rather than sending
149
+ \`null\` or \`0\`. \`statusMessage\`, \`passbackParams\`, \`fee\`, \`feeFixed\` and \`feeRate\` are all simply
150
+ missing when they do not apply. A missing \`fee\` means the fee is *unknown* for that transaction - it
151
+ does not mean the transaction was free. Do not write a handler that reads \`payload.fee\` as a number
152
+ without checking it is there.
153
+
154
+ **2. \`finalized\` is absent on EXPIRED.** An expired transaction is closed by the platform's expiry
155
+ sweep, which sets no finalization moment. Every other status carries one.
156
+
157
+ **3. \`EXPIRED\` is a non-payment, not a failure.** The payer never paid within \`timeToLive\`. Release
158
+ the order or the stock, as you would for a cancellation - do not treat it as an error to retry.
159
+
160
+ **4. Deduplicate on \`(transactionId, callbackType)\`.** payware retries a callback up to 15 times,
161
+ backing off from about a second to at most 5 minutes between attempts, until the endpoint answers any
162
+ 2xx. That pair is stable across every retry of the same event, and it is the key payware guarantees.
163
+ A handler that does not deduplicate will eventually process one payment twice.
164
+
165
+ **Not on this payload:** \`deliveryAddress\` (removed 2026-08-07 - fetch it from
166
+ \`payware_operations_get_transaction_history\`, which is authenticated and tenant-scoped),
167
+ \`producerPartnerId\`, \`transactionType\` and \`initiatedBy\` (the last two are carried on
168
+ TRANSACTION_PROCESSED, which merchants do not receive).`,
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: {
172
+ transactionId: {
173
+ type: "string",
174
+ description: "Transaction ID to simulate callback for"
175
+ },
176
+ status: {
177
+ type: "string",
178
+ enum: ["CONFIRMED", "DECLINED", "FAILED", "EXPIRED", "CANCELLED"],
179
+ description: "Transaction status to simulate",
180
+ default: "CONFIRMED"
181
+ },
182
+ callbackUrl: {
183
+ type: "string",
184
+ description: "URL where callback would be sent (for simulation only)",
185
+ format: "uri"
186
+ },
187
+ amount: {
188
+ type: "string",
189
+ description: "Transaction amount (e.g., '57.60')",
190
+ default: "57.60"
191
+ },
192
+ currency: {
193
+ type: "string",
194
+ enum: ["EUR", "USD", "GBP"],
195
+ description: "Transaction currency",
196
+ default: "EUR"
197
+ },
198
+ fee: {
199
+ type: "string",
200
+ 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'."
201
+ },
202
+ feeFixed: {
203
+ type: "string",
204
+ 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.",
205
+ default: "0.1000"
206
+ },
207
+ feeRate: {
208
+ type: "string",
209
+ 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.",
210
+ default: "0.0150"
211
+ },
212
+ statusMessage: {
213
+ type: "string",
214
+ description: "Custom status message for the transaction callback"
215
+ },
216
+ paymentMethod: {
217
+ type: "string",
218
+ enum: ["A2A", "CARD_FUNDED", "BNPL", "INSTANT_CREDIT"],
219
+ description: "Payment method chosen by customer. A2A = direct transfer. CARD_FUNDED = card-linked account. BNPL = buy now pay later. INSTANT_CREDIT = credit line."
220
+ },
221
+ poiId: {
222
+ type: "string",
223
+ description: "Set to simulate a POI sale: the callback then carries the poiId of the POI that created the transaction"
224
+ }
225
+ },
226
+ required: ["transactionId"],
227
+ additionalProperties: false
228
+ },
229
+
230
+ async handler(args) {
231
+ const {
232
+ transactionId,
233
+ status = 'CONFIRMED',
234
+ callbackUrl,
235
+ amount = '57.60',
236
+ currency = 'EUR',
237
+ // No default: an omitted fee falls through to generateMockCallback's own derivation.
238
+ // Defaulting it to '0.00' here made every simulated callback assert a zero fee, which is the
239
+ // one value the real payload never sends for "no fee" - it omits the property instead.
240
+ fee,
241
+ feeFixed = '0.1000',
242
+ feeRate = '0.0150',
243
+ statusMessage,
244
+ paymentMethod,
245
+ poiId
246
+ } = args;
247
+
248
+ if (!transactionId) {
249
+ throw new Error("Transaction ID is required");
250
+ }
251
+
252
+ // Generate mock callback payload
253
+ const payload = generateMockCallback(transactionId, status, {
254
+ amount,
255
+ currency,
256
+ fee,
257
+ feeFixed,
258
+ feeRate,
259
+ statusMessage,
260
+ paymentMethod,
261
+ poiId
262
+ });
263
+
264
+ // Simulate callback delivery if URL provided
265
+ let deliveryResult = null;
266
+ if (callbackUrl) {
267
+ deliveryResult = await simulateCallbackDelivery(callbackUrl, payload);
268
+ }
269
+
270
+ const statusEmojis = {
271
+ 'CONFIRMED': '✅',
272
+ 'DECLINED': '❌',
273
+ 'FAILED': '💥',
274
+ 'EXPIRED': '⏰',
275
+ 'CANCELLED': '🚫'
276
+ };
277
+
278
+ const statusEmoji = statusEmojis[status] || '❓';
279
+
280
+ return {
281
+ content: [{
282
+ type: "text",
283
+ text: `🔄 **Callback Simulation Complete**
284
+
285
+ **Transaction:** ${transactionId}
286
+ **Simulated Status:** ${statusEmoji} ${status}
287
+
288
+ **Mock Callback Payload:**
289
+ \`\`\`json
290
+ ${JSON.stringify(payload, null, 2)}
291
+ \`\`\`
292
+
293
+ ${deliveryResult ? `
294
+ **Callback Delivery Simulation:**
295
+ ${deliveryResult.success ? '✅ Callback would be delivered successfully' : '❌ Callback delivery would fail'}
296
+
297
+ **Delivery Details:**
298
+ - URL: ${deliveryResult.url || 'N/A'}
299
+ - Headers: ${JSON.stringify(deliveryResult.headers || {}, null, 2)}
300
+ - Simulated At: ${deliveryResult.simulatedAt || 'N/A'}
301
+
302
+ **Note:** ${deliveryResult.note || 'No actual HTTP request was made'}
303
+ ` : '**No callback URL provided** - only payload generated'}
304
+
305
+ **Status Explanations:**
306
+ - ✅ **CONFIRMED**: Successfully finalized
307
+ - ❌ **DECLINED**: Declined by the user, processing or finalizing payment institutions
308
+ - 💥 **FAILED**: Failed due to technical reasons or other
309
+ - ⏰ **EXPIRED**: Time to live of the transaction has passed
310
+ - 🚫 **CANCELLED**: Transaction canceled by the originator
311
+
312
+ **Integration Testing:**
313
+ 1. Use this payload to test your callback handler
314
+ 2. Verify your system processes each status correctly
315
+ 3. Check error handling for DECLINED/FAILED statuses
316
+ 4. Ensure proper logging and monitoring
317
+
318
+ **Next Steps:**
319
+ 1. Implement callback endpoint to receive this payload
320
+ 2. Test with different status scenarios
321
+ 3. Add signature verification in production
322
+ 4. Set up proper error handling and retry logic`
323
+ }]
324
+ };
325
+ }
314
326
  };