@payware/mcp-server 1.1.1 → 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 (127) hide show
  1. package/.env.example +0 -0
  2. package/README.md +22 -16
  3. package/check-keys.js +0 -0
  4. package/keys/README.md +0 -0
  5. package/mcp-config.json +0 -0
  6. package/package.json +69 -66
  7. package/payware-mcp-config.json +0 -0
  8. package/src/auth/rsa-keys.js +0 -0
  9. package/src/auth/sandbox-setup.js +186 -186
  10. package/src/config/env.js +0 -0
  11. package/src/config/partner-types.js +0 -0
  12. package/src/core/auth/jwt-factory.js +4 -4
  13. package/src/core/auth/jwt-token.js +107 -66
  14. package/src/core/utils/json-serializer.js +5 -5
  15. package/src/core/utils/key-utils.js +0 -0
  16. package/src/index.js +0 -0
  17. package/src/isv/data/index.js +0 -0
  18. package/src/isv/deep-links/delete-product-link.js +0 -0
  19. package/src/isv/deep-links/delete-transaction-link.js +0 -0
  20. package/src/isv/deep-links/get-product-link.js +0 -0
  21. package/src/isv/deep-links/get-transaction-link.js +0 -0
  22. package/src/isv/deep-links/index.js +0 -0
  23. package/src/isv/index.js +29 -1
  24. package/src/isv/invitations/index.js +471 -0
  25. package/src/isv/oauth2/create-token-simple.js +0 -0
  26. package/src/isv/oauth2/get-token-info.js +0 -0
  27. package/src/isv/oauth2/obtain-token.js +19 -8
  28. package/src/isv/oauth2/token-helper.js +5 -2
  29. package/src/isv/poi/cancel-price.js +2 -10
  30. package/src/isv/poi/create.js +19 -0
  31. package/src/isv/poi/get-poi.js +2 -10
  32. package/src/isv/poi/get-qrcode.js +2 -10
  33. package/src/isv/poi/get-status.js +2 -10
  34. package/src/isv/poi/index.js +5 -0
  35. package/src/isv/poi/list-pois.js +2 -10
  36. package/src/isv/poi/set-price.js +217 -225
  37. package/src/isv/products/audios.js +0 -0
  38. package/src/isv/products/create-product.js +313 -313
  39. package/src/isv/products/delete-product.js +0 -0
  40. package/src/isv/products/get-product-image.js +0 -0
  41. package/src/isv/products/get-product.js +0 -0
  42. package/src/isv/products/index.js +0 -0
  43. package/src/isv/products/list-products.js +0 -0
  44. package/src/isv/products/schedules.js +0 -0
  45. package/src/isv/products/update-product.js +264 -264
  46. package/src/isv/reference/index.js +249 -0
  47. package/src/isv/shops/index.js +22 -0
  48. package/src/isv/transactions/cancel-transaction.js +43 -12
  49. package/src/isv/transactions/create-transaction.js +22 -6
  50. package/src/isv/transactions/get-status.js +18 -11
  51. package/src/isv/transactions/process-transaction.js +160 -42
  52. package/src/isv/transactions/simulate-callback.js +128 -109
  53. package/src/isv/transactions/transaction-history.js +86 -90
  54. package/src/isv/utils/auth.js +2 -2
  55. package/src/mcp-proxy.js +0 -0
  56. package/src/merchant/data/export-report.js +0 -0
  57. package/src/merchant/data/generate-report.js +19 -1
  58. package/src/merchant/data/get-export-list.js +0 -0
  59. package/src/merchant/data/get-report-requests.js +0 -0
  60. package/src/merchant/data/get-report-status.js +0 -0
  61. package/src/merchant/data/index.js +0 -0
  62. package/src/merchant/data/list-reports.js +0 -0
  63. package/src/merchant/deep-links/delete-product-link.js +0 -0
  64. package/src/merchant/deep-links/delete-transaction-link.js +0 -0
  65. package/src/merchant/deep-links/get-product-link.js +0 -0
  66. package/src/merchant/deep-links/get-transaction-link.js +0 -0
  67. package/src/merchant/deep-links/index.js +0 -0
  68. package/src/merchant/index.js +21 -1
  69. package/src/merchant/logs/index.js +366 -0
  70. package/src/merchant/products/audios.js +0 -0
  71. package/src/merchant/products/create-product.js +287 -287
  72. package/src/merchant/products/delete-product.js +0 -0
  73. package/src/merchant/products/get-product-image.js +0 -0
  74. package/src/merchant/products/get-product.js +0 -0
  75. package/src/merchant/products/index.js +0 -0
  76. package/src/merchant/products/list-products.js +0 -0
  77. package/src/merchant/products/schedules.js +0 -0
  78. package/src/merchant/products/update-product.js +281 -281
  79. package/src/merchant/shops/index.js +18 -0
  80. package/src/merchant/transactions/cancel-transaction.js +16 -19
  81. package/src/merchant/transactions/create-transaction.js +99 -19
  82. package/src/merchant/transactions/get-status.js +41 -12
  83. package/src/merchant/transactions/process-transaction.js +8 -16
  84. package/src/merchant/transactions/simulate-callback.js +68 -9
  85. package/src/merchant/transactions/transaction-history.js +275 -257
  86. package/src/payment-institution/data/index.js +0 -0
  87. package/src/payment-institution/deep-links/delete-transaction-link.js +0 -0
  88. package/src/payment-institution/deep-links/get-transaction-link.js +0 -0
  89. package/src/payment-institution/deep-links/index.js +0 -0
  90. package/src/payment-institution/index.js +0 -0
  91. package/src/payment-institution/transactions/create-pi-transaction.js +6 -14
  92. package/src/payment-institution/transactions/finalize-transaction.js +333 -340
  93. package/src/payment-institution/transactions/get-transaction-history.js +3 -11
  94. package/src/payment-institution/transactions/get-transaction-status.js +39 -12
  95. package/src/payment-institution/transactions/process-pi-transaction.js +5 -13
  96. package/src/payment-institution/transactions/simulate-pi-callback.js +49 -9
  97. package/src/payment-institution/transactions/soundbite-transaction.js +5 -13
  98. package/src/proxy-server.js +0 -0
  99. package/src/shared/api-errors.js +188 -0
  100. package/src/shared/auth-headers.js +62 -0
  101. package/src/shared/data/data-api.js +11 -10
  102. package/src/shared/deep-links/api.js +5 -4
  103. package/src/shared/index.js +0 -0
  104. package/src/shared/logs/logs-api.js +242 -0
  105. package/src/shared/poi/poi-create-api.js +419 -0
  106. package/src/shared/products/products-api.js +17 -16
  107. package/src/shared/shops/shops-api.js +155 -0
  108. package/src/tools/advanced-code-generator.js +2255 -2246
  109. package/src/utils/code-examples/common/auth-examples.js +1484 -1481
  110. package/src/utils/code-examples/common/helpers.js +7 -4
  111. package/src/utils/code-examples/common/utils-examples.js +0 -0
  112. package/src/utils/code-examples/frameworks/examples.js +4 -4
  113. package/src/utils/code-examples/index.js +0 -0
  114. package/src/utils/code-examples/isv/oauth2.js +458 -600
  115. package/src/utils/code-examples/isv/products.js +8 -8
  116. package/src/utils/code-examples/isv/transactions.js +815 -812
  117. package/src/utils/code-examples/merchant/products.js +0 -0
  118. package/src/utils/code-examples/merchant/transactions.js +9 -9
  119. package/src/utils/code-examples/payment-institution/transactions.js +0 -0
  120. package/src/utils/code-examples/shared/data.js +0 -0
  121. package/src/utils/code-examples/shared/deep-links.js +0 -0
  122. package/src/utils/code-examples/shared/soundbites.js +0 -0
  123. package/src/utils/code-examples.js +24 -24
  124. package/src/utils/format-request.js +3 -3
  125. package/src/utils/json-formatter.js +20 -20
  126. package/src/utils/server-info.js +0 -0
  127. package/MCP-SETUP.md +0 -190
@@ -1,15 +1,66 @@
1
1
  import axios from 'axios';
2
2
  import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
3
+ import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
3
4
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
5
+ import { describeApiError } from '../../shared/api-errors.js';
4
6
 
5
7
  /**
6
- * Process (finalize) a transaction via payware API as an ISV
8
+ * Process a PEER transaction as an ISV on behalf of a merchant.
9
+ *
10
+ * **This used to send `PATCH /transactions/{id}` with `{action: "CONFIRMED"|"DECLINED"}`, which is
11
+ * wrong three times over.**
12
+ *
13
+ * 1. Wrong verb and operation. `PATCH` is *finalize*, not process. Processing is
14
+ * `POST /transactions/{transactionId}` with a full transaction request - which is what the
15
+ * merchant tool has always done.
16
+ * 2. Wrong field. Finalize consumes `TransactionStatusUpdateRequest` - `status`, `statusMessage`,
17
+ * `currency`, `amount`, `fee`. There is no `action` field anywhere on the payware API, so the
18
+ * body deserialized to an empty request.
19
+ * 3. Wrong actor. Even with the right field name, a merchant or an ISV acting for one may only
20
+ * send `status: CANCELLED`: `MerchantValidationService` throws `InvalidStatusException` for
21
+ * anything else, because confirming or declining a payment is the financial institution's
22
+ * decision and merchants never reach that path.
23
+ *
24
+ * To cancel, use `payware_operations_cancel_transaction`. To confirm or decline, you are looking for
25
+ * the financial-institution role's `payware_operations_finalize_transaction`, which an ISV is not.
7
26
  */
8
- export async function processTransaction({ transactionId, merchantPartnerId, oauth2Token, action = 'CONFIRMED', useSandbox = true }) {
27
+ export async function processTransaction({
28
+ transactionId,
29
+ amount,
30
+ currency,
31
+ reasonL1,
32
+ reasonL2,
33
+ account,
34
+ friendlyName,
35
+ shop,
36
+ callbackUrl,
37
+ passbackParams,
38
+ timeToLive = 120,
39
+ paymentMethod,
40
+ merchantPartnerId,
41
+ oauth2Token,
42
+ useSandbox = true
43
+ }) {
9
44
  if (!transactionId) {
10
45
  throw new Error('Transaction ID is required');
11
46
  }
12
47
 
48
+ if (!amount) {
49
+ throw new Error('Amount is required for processing');
50
+ }
51
+
52
+ if (typeof amount !== 'string' && typeof amount !== 'number') {
53
+ throw new Error('Amount must be a string or number representing currency value (e.g., "25.50" or 25.50)');
54
+ }
55
+
56
+ if (parseFloat(amount) <= 0) {
57
+ throw new Error('Amount must be positive for processing');
58
+ }
59
+
60
+ if (!reasonL1) {
61
+ throw new Error('reasonL1 is required (transaction grounds description)');
62
+ }
63
+
13
64
  if (!merchantPartnerId) {
14
65
  throw new Error('Merchant Partner ID is required for ISV operations');
15
66
  }
@@ -18,14 +69,27 @@ export async function processTransaction({ transactionId, merchantPartnerId, oau
18
69
  throw new Error('OAuth2 token is required for ISV operations');
19
70
  }
20
71
 
21
- if (!['CONFIRMED', 'DECLINED'].includes(action)) {
22
- throw new Error('Action must be either CONFIRMED or DECLINED');
23
- }
24
-
25
72
  const isvPartnerId = getPartnerIdSafe();
26
73
  const privateKey = getPrivateKeySafe(useSandbox);
27
74
 
28
- const requestBody = { action };
75
+ // Same body the merchant tool builds - the on-behalf claims live in the JWT, not in the payload.
76
+ const requestBody = {
77
+ ...(account && { account }),
78
+ ...(friendlyName && { friendlyName }),
79
+ ...(shop && { shop }),
80
+ ...(callbackUrl && { callbackUrl }),
81
+ ...(passbackParams && { passbackParams }),
82
+ ...(paymentMethod && { paymentMethod }),
83
+ trData: {
84
+ amount: amount.toString(),
85
+ currency,
86
+ reasonL1,
87
+ ...(reasonL2 && { reasonL2 })
88
+ },
89
+ trOptions: {
90
+ timeToLive
91
+ }
92
+ };
29
93
 
30
94
  const jwtData = await createJWTForPartner({
31
95
  partnerId: isvPartnerId,
@@ -35,14 +99,12 @@ export async function processTransaction({ transactionId, merchantPartnerId, oau
35
99
  oauth2Token
36
100
  });
37
101
 
38
- // Use the same JSON serialization method as the JWT factory for MD5 calculation
39
- const { createMinimizedJSON } = await import('../../core/utils/json-serializer.js');
102
+ // The EXACT string the JWT hashed must be the body; anything else is ERR_INVALID_CONTENT_HASH.
40
103
  const minimizedBodyString = createMinimizedJSON(requestBody);
41
104
 
42
105
  try {
43
106
  const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
44
- // Send the exact minimized JSON string that was used for MD5 calculation
45
- const response = await axios.patch(`${baseUrl}/transactions/${transactionId}`, minimizedBodyString, {
107
+ const response = await axios.post(`${baseUrl}/transactions/${transactionId}`, minimizedBodyString, {
46
108
  headers: {
47
109
  'Authorization': `Bearer ${jwtData.token}`,
48
110
  'Content-Type': 'application/json',
@@ -54,42 +116,102 @@ export async function processTransaction({ transactionId, merchantPartnerId, oau
54
116
 
55
117
  return response.data;
56
118
  } catch (error) {
57
- if (error.response) {
58
- throw new Error(`payware API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
59
- }
60
- throw error;
119
+ throw describeApiError(error, 'call the payware API');
61
120
  }
62
121
  }
63
122
 
64
123
  export const processTransactionTool = {
65
124
  name: 'payware_operations_process_transaction',
66
- description: `Process (finalize) a transaction as an ISV on behalf of a merchant.
125
+ description: `Process a PEER transaction as an ISV on behalf of a merchant - the merchant scanning a customer's QR or barcode to collect payment.
126
+
127
+ **Endpoint:** POST /transactions/{transactionId}
128
+
129
+ The customer presents a transaction they created; the merchant (through you) processes it with the
130
+ amount to collect. Only ACTIVE transactions can be processed - one already processed answers **409
131
+ \`ERR_ALREADY_PROCESSED\`**, which means "already done", not "bad request".
132
+
133
+ ⚠️ **This does not confirm or decline a payment.** Processing hands the transaction to the payer's
134
+ financial institution, which decides. An ISV cannot send CONFIRMED or DECLINED - the server rejects
135
+ any status other than CANCELLED from a merchant or its ISV. To cancel, use
136
+ \`payware_operations_cancel_transaction\`.
137
+
138
+ ⚠️ **The MERCHANT's plan governs, not the ISV's.** An on-behalf call inherits the acting merchant's
139
+ plan, so this answers **403** when that merchant is on **Basic**. Naming a shop outside the scope the
140
+ merchant assigned you answers **403 \`ERR_SHOP_NOT_IN_SCOPE\`**.
67
141
 
68
- Allows confirming or declining a transaction. Only ACTIVE transactions can be processed.
69
- Requires ISV authentication with merchant partner ID and OAuth2 token.`,
142
+ 💰 **\`amount\` is a decimal string at the currency's own scale** - not always two decimals. The fee
143
+ payware returns on the response is the value to echo back at finalize, unchanged.`,
70
144
 
71
145
  inputSchema: {
72
146
  type: 'object',
73
- required: ['transactionId', 'merchantPartnerId', 'oauth2Token'],
147
+ required: ['transactionId', 'amount', 'currency', 'reasonL1', 'merchantPartnerId', 'oauth2Token'],
74
148
  properties: {
75
149
  transactionId: {
76
150
  type: 'string',
77
- description: 'Transaction ID to process'
151
+ description: "The customer's transaction to process, e.g. \"pw7e4rCToG\""
152
+ },
153
+ amount: {
154
+ type: 'string',
155
+ description: 'Amount to collect, as a decimal string at the currency\'s own scale (e.g. "25.50"). Must be positive.'
156
+ },
157
+ currency: {
158
+ type: 'string',
159
+ pattern: '^[A-Z]{3}$',
160
+ description: 'ISO 4217 currency code. Must match the transaction currency - changing it answers ERR_SPECIFYING_CURRENCY_NOT_ALLOWED.'
161
+ },
162
+ reasonL1: {
163
+ type: 'string',
164
+ description: 'Transaction grounds description (required)',
165
+ maxLength: 100
166
+ },
167
+ reasonL2: {
168
+ type: 'string',
169
+ description: 'Transaction grounds description continuation (optional)',
170
+ maxLength: 100
171
+ },
172
+ account: {
173
+ type: 'string',
174
+ description: 'Merchant account identifier. If omitted, determined from the merchant profile.',
175
+ maxLength: 36
176
+ },
177
+ friendlyName: {
178
+ type: 'string',
179
+ description: 'Account holder recognizable name. If omitted, the merchant alias is used.',
180
+ maxLength: 100
181
+ },
182
+ shop: {
183
+ type: 'string',
184
+ description: 'Shop code. If omitted, the merchant default shop is used - which may be outside your assigned scope. Find valid codes with payware_shops_list.',
185
+ maxLength: 10
186
+ },
187
+ callbackUrl: {
188
+ type: 'string',
189
+ description: 'HTTPS URL for the status callback. Must present a publicly-trusted TLS certificate and resolve to a public address.',
190
+ format: 'uri'
191
+ },
192
+ passbackParams: {
193
+ type: 'string',
194
+ description: 'Opaque string passed back unmodified on responses and callbacks.',
195
+ maxLength: 200
196
+ },
197
+ timeToLive: {
198
+ type: 'number',
199
+ description: 'Processing window in seconds.',
200
+ default: 120
201
+ },
202
+ paymentMethod: {
203
+ type: 'string',
204
+ enum: ['A2A', 'CARD_FUNDED', 'BNPL', 'INSTANT_CREDIT'],
205
+ description: 'Payment method. A2A = direct transfer. CARD_FUNDED = card-linked account. BNPL = buy now pay later. INSTANT_CREDIT = credit line.'
78
206
  },
79
207
  merchantPartnerId: {
80
208
  type: 'string',
81
- description: 'Partner ID of the target merchant'
209
+ description: 'Partner ID of the target merchant (8 alphanumeric characters)'
82
210
  },
83
211
  oauth2Token: {
84
212
  type: 'string',
85
213
  description: 'OAuth2 access token obtained from the merchant'
86
214
  },
87
- action: {
88
- type: 'string',
89
- enum: ['CONFIRMED', 'DECLINED'],
90
- default: 'CONFIRMED',
91
- description: 'Action to perform on the transaction'
92
- },
93
215
  useSandbox: {
94
216
  type: 'boolean',
95
217
  description: 'Use sandbox environment for testing',
@@ -102,19 +224,19 @@ Requires ISV authentication with merchant partner ID and OAuth2 token.`,
102
224
  try {
103
225
  const result = await processTransaction(params);
104
226
 
105
- const actionEmoji = params.action === 'CONFIRMED' ? '✅' : '❌';
106
-
107
227
  return {
108
228
  content: [{
109
229
  type: 'text',
110
- text: `${actionEmoji} Transaction ${params.action} Successfully (ISV -> Merchant: ${params.merchantPartnerId})
230
+ text: `✅ **Transaction Processed** (ISV -> Merchant: ${params.merchantPartnerId})
111
231
 
112
- 📋 **Processing Details:**
113
- - **Transaction ID**: ${params.transactionId}
114
- - **Action**: ${params.action}
115
- - **New Status**: ${result.status || params.action}
116
- - **ISV Partner**: ${getPartnerIdSafe()}
117
- - **Target Merchant**: ${params.merchantPartnerId}
232
+ - Transaction ID: ${result.transactionId || params.transactionId}
233
+ - Status: ${result.status || 'N/A'}
234
+ - Amount: ${result.amount ?? params.amount} ${result.currency ?? params.currency}
235
+ - Fee: ${result.fee ?? '(not reported)'}${result.fee ? ' - echo this back unchanged at finalize' : ''}
236
+ - ISV Partner: ${getPartnerIdSafe()}
237
+
238
+ **Next:** the payer's financial institution now confirms or declines. Watch for the callback, or poll
239
+ \`payware_operations_get_transaction_status\` while it is ACTIVE.
118
240
 
119
241
  **Raw Response:**
120
242
  \`\`\`json
@@ -132,14 +254,10 @@ ${JSON.stringify(result, null, 2)}
132
254
 
133
255
  **Processing Attempt:**
134
256
  - Transaction ID: ${params.transactionId}
135
- - Action: ${params.action}
136
- - Merchant: ${params.merchantPartnerId}
137
-
138
- **ISV Authentication:**
139
- - OAuth2 Token: ${params.oauth2Token ? 'Provided' : 'Missing'}
257
+ - Merchant Partner ID: ${params.merchantPartnerId}
140
258
  - ISV Partner ID: ${getPartnerIdSafe()}`
141
259
  }]
142
260
  };
143
261
  }
144
262
  }
145
- };
263
+ };
@@ -1,146 +1,165 @@
1
- import axios from 'axios';
2
- import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
3
- import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
1
+ import { getPartnerIdSafe } from '../../config/env.js';
2
+ import { generateMockCallback, simulateCallbackDelivery } from '../../merchant/transactions/simulate-callback.js';
4
3
 
5
4
  /**
6
- * Simulate a transaction callback via payware API as an ISV
5
+ * Simulate the callback a merchant's endpoint receives, for an ISV building that handler.
6
+ *
7
+ * **This used to POST `/transactions/{id}/simulate-callback` with `{action: "CONFIRMED"}`.** There is
8
+ * no such endpoint - a whole-tree search of the server finds no `simulate-callback` route in any
9
+ * controller, in any profile - so every call 404'd, and the `action` field it sent is not a field on
10
+ * any payware request. payware has no "trigger a callback for me" API at all.
11
+ *
12
+ * Simulation is therefore local, exactly as the merchant tool has always done it: build the payload
13
+ * payware would send and hand it back, so a handler can be written and tested against the real
14
+ * shape without a live transaction. The generator is imported from the merchant tool rather than
15
+ * copied - one definition of the payload, so a contract change cannot fix one role and miss the
16
+ * other, which is how the two drifted apart in the first place.
7
17
  */
8
- export async function simulateCallback({ transactionId, merchantPartnerId, oauth2Token, action = 'CONFIRMED', useSandbox = true }) {
9
- if (!transactionId) {
10
- throw new Error('Transaction ID is required');
11
- }
18
+ export const simulateCallbackTool = {
19
+ name: 'payware_operations_simulate_callback',
20
+ description: `Simulate the callback payload payware sends to a merchant's callbackUrl, for building and testing a webhook handler as an ISV.
12
21
 
13
- if (!merchantPartnerId) {
14
- throw new Error('Merchant Partner ID is required for ISV operations');
15
- }
22
+ **Local only - this makes no API call.** payware has no endpoint that triggers a callback on demand,
23
+ so nothing here reaches the network. What you get back is the exact payload shape a real
24
+ TRANSACTION_FINALIZED callback carries, which is what a handler needs in order to be written.
16
25
 
17
- if (!oauth2Token) {
18
- throw new Error('OAuth2 token is required for ISV operations');
19
- }
26
+ **Callbacks go to the MERCHANT's callbackUrl, not to yours.** An ISV that needs to know a transaction
27
+ finished either has the merchant forward it, or polls
28
+ \`payware_operations_get_transaction_history\` for the ids it created. Simulating the payload here
29
+ tells you what the merchant's endpoint will receive; it does not route anything to you.
20
30
 
21
- if (!['CONFIRMED', 'DECLINED'].includes(action)) {
22
- throw new Error('Action must be either CONFIRMED or DECLINED');
23
- }
31
+ Four properties of the real payload that are easy to get wrong:
24
32
 
25
- const isvPartnerId = getPartnerIdSafe();
26
- const privateKey = getPrivateKeySafe(useSandbox);
27
-
28
- const requestBody = { action };
29
-
30
- const jwtData = await createJWTForPartner({
31
- partnerId: isvPartnerId,
32
- privateKey,
33
- requestBody,
34
- merchantId: merchantPartnerId,
35
- oauth2Token
36
- });
37
-
38
- // Use the same JSON serialization method as the JWT factory for MD5 calculation
39
- const { createMinimizedJSON } = await import('../../core/utils/json-serializer.js');
40
- const minimizedBodyString = createMinimizedJSON(requestBody);
41
-
42
- try {
43
- const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
44
- // Send the exact minimized JSON string that was used for MD5 calculation
45
- const response = await axios.post(`${baseUrl}/transactions/${transactionId}/simulate-callback`, minimizedBodyString, {
46
- headers: {
47
- 'Authorization': `Bearer ${jwtData.token}`,
48
- 'Content-Type': 'application/json',
49
- 'Api-Version': '1'
50
- },
51
- // Tell axios to send the string as-is, don't serialize it again
52
- transformRequest: [(data) => data]
53
- });
33
+ **1. Absent means absent, not zero.** Properties with no value are omitted, not sent as \`null\` or
34
+ \`0\`. A missing \`fee\` means the fee is *unknown* for that transaction - not that it was free.
54
35
 
55
- return response.data;
56
- } catch (error) {
57
- if (error.response) {
58
- throw new Error(`payware API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
59
- }
60
- throw error;
61
- }
62
- }
36
+ **2. \`finalized\` is absent on EXPIRED** - the expiry sweep sets no finalization moment.
63
37
 
64
- export const simulateCallbackTool = {
65
- name: 'payware_operations_simulate_callback',
66
- description: `Simulate a transaction callback as an ISV on behalf of a merchant.
38
+ **3. \`EXPIRED\` is a non-payment, not a failure.** The payer never paid within \`timeToLive\`. Release
39
+ the order or the stock; do not treat it as an error to retry.
67
40
 
68
- Triggers a callback to test webhook integration in sandbox environment.
69
- Requires ISV authentication with merchant partner ID and OAuth2 token.`,
41
+ **4. Deduplicate on \`(transactionId, callbackType)\`.** payware retries once a second, up to 15
42
+ times, until the endpoint answers 200. That pair is stable across retries and is the key payware
43
+ guarantees.
44
+
45
+ **Not on this payload:** \`deliveryAddress\` (removed 2026-08-07 - fetch it from
46
+ \`payware_operations_get_transaction_history\`), \`producerPartnerId\`, \`transactionType\` and
47
+ \`initiatedBy\`.`,
70
48
 
71
49
  inputSchema: {
72
50
  type: 'object',
73
- required: ['transactionId', 'merchantPartnerId', 'oauth2Token'],
51
+ required: ['transactionId'],
74
52
  properties: {
75
53
  transactionId: {
76
54
  type: 'string',
77
- description: 'Transaction ID to simulate callback for'
55
+ description: 'Transaction ID to build the callback payload for'
78
56
  },
79
- merchantPartnerId: {
57
+ status: {
58
+ type: 'string',
59
+ enum: ['CONFIRMED', 'DECLINED', 'FAILED', 'EXPIRED', 'CANCELLED'],
60
+ description: 'Final status to simulate',
61
+ default: 'CONFIRMED'
62
+ },
63
+ callbackUrl: {
80
64
  type: 'string',
81
- description: 'Partner ID of the target merchant'
65
+ description: 'URL the callback would be delivered to. Recorded in the output for reference; no request is sent.',
66
+ format: 'uri'
82
67
  },
83
- oauth2Token: {
68
+ amount: {
84
69
  type: 'string',
85
- description: 'OAuth2 access token obtained from the merchant'
70
+ description: "Transaction amount as a decimal string at the currency's own scale (e.g. '57.60')",
71
+ default: '57.60'
86
72
  },
87
- action: {
73
+ currency: {
88
74
  type: 'string',
89
- enum: ['CONFIRMED', 'DECLINED'],
90
- default: 'CONFIRMED',
91
- description: 'Action to simulate in the callback'
75
+ enum: ['EUR', 'USD', 'GBP'],
76
+ description: 'Transaction currency',
77
+ default: 'EUR'
78
+ },
79
+ fee: {
80
+ type: 'string',
81
+ description: "The payware fee, at the currency's smallest payable unit (e.g. '0.29'). Pass null to model the anomaly case where a transaction has no recorded fee and the server OMITS the property - deliberately distinguishable from a genuine '0.00'."
82
+ },
83
+ feeFixed: {
84
+ type: 'string',
85
+ description: 'Fixed fee component of the applicable fee configuration. Informational, for reconciliation. Present on FINALIZED callbacks since 2026-08-21.',
86
+ default: '0.1000'
87
+ },
88
+ feeRate: {
89
+ type: 'string',
90
+ description: "Variable rate of the applicable fee configuration, e.g. '0.0150' = 1.5%. Informational, same as feeFixed.",
91
+ default: '0.0150'
92
+ },
93
+ statusMessage: {
94
+ type: 'string',
95
+ description: 'Custom status message. DECLINED, FAILED and CANCELLED transactions always carry one.'
96
+ },
97
+ paymentMethod: {
98
+ type: 'string',
99
+ enum: ['A2A', 'CARD_FUNDED', 'BNPL', 'INSTANT_CREDIT'],
100
+ description: 'Payment method chosen by the customer. A2A = direct transfer. CARD_FUNDED = card-linked account. BNPL = buy now pay later. INSTANT_CREDIT = credit line.'
101
+ },
102
+ merchantPartnerId: {
103
+ type: 'string',
104
+ description: 'Partner ID of the merchant whose callback this simulates. Used for labelling only - no call is made on their behalf.'
92
105
  },
93
106
  useSandbox: {
94
107
  type: 'boolean',
95
- description: 'Use sandbox environment for testing',
108
+ description: 'Retained for consistency with the other tools; simulation is local and ignores it.',
96
109
  default: true
97
110
  }
98
111
  }
99
112
  },
100
113
 
101
- async handler(params) {
102
- try {
103
- const result = await simulateCallback(params);
114
+ async handler(args) {
115
+ const {
116
+ transactionId,
117
+ status = 'CONFIRMED',
118
+ callbackUrl,
119
+ amount = '57.60',
120
+ currency = 'EUR',
121
+ fee,
122
+ feeFixed = '0.1000',
123
+ feeRate = '0.0150',
124
+ statusMessage,
125
+ paymentMethod,
126
+ merchantPartnerId
127
+ } = args;
128
+
129
+ if (!transactionId) {
130
+ throw new Error('Transaction ID is required');
131
+ }
104
132
 
105
- const actionEmoji = params.action === 'CONFIRMED' ? '✅' : '❌';
133
+ const payload = generateMockCallback(transactionId, status, {
134
+ amount, currency, fee, feeFixed, feeRate, statusMessage, paymentMethod
135
+ });
106
136
 
107
- return {
108
- content: [{
109
- type: 'text',
110
- text: `🔔 Callback Simulated Successfully (ISV -> Merchant: ${params.merchantPartnerId})
137
+ const delivery = callbackUrl ? await simulateCallbackDelivery(callbackUrl, payload) : null;
111
138
 
112
- **Callback Details:**
113
- - **Transaction ID**: ${params.transactionId}
114
- - **Simulated Action**: ${actionEmoji} ${params.action}
115
- - **ISV Partner**: ${getPartnerIdSafe()}
116
- - **Target Merchant**: ${params.merchantPartnerId}
139
+ return {
140
+ content: [{
141
+ type: 'text',
142
+ text: `🔔 **Simulated Callback Payload**${merchantPartnerId ? ` (ISV ${getPartnerIdSafe()} -> Merchant ${merchantPartnerId})` : ''}
117
143
 
118
- **Use Case**: Testing webhook integration in sandbox environment
144
+ This is what payware would POST to the **merchant's** \`callbackUrl\`. No request was made.
119
145
 
120
- **Raw Response:**
121
146
  \`\`\`json
122
- ${JSON.stringify(result, null, 2)}
123
- \`\`\``
124
- }]
125
- };
126
- } catch (error) {
127
- return {
128
- content: [{
129
- type: 'text',
130
- text: `❌ Callback Simulation Failed
131
-
132
- **Error**: ${error.message}
133
-
134
- **Simulation Attempt:**
135
- - Transaction ID: ${params.transactionId}
136
- - Action: ${params.action}
137
- - Merchant: ${params.merchantPartnerId}
138
-
139
- **ISV Authentication:**
140
- - OAuth2 Token: ${params.oauth2Token ? 'Provided' : 'Missing'}
141
- - ISV Partner ID: ${getPartnerIdSafe()}`
142
- }]
143
- };
144
- }
147
+ ${JSON.stringify(payload, null, 2)}
148
+ \`\`\`
149
+ ${delivery ? `
150
+ **Would be delivered to:** ${delivery.url}
151
+ **Headers:** ${JSON.stringify(delivery.headers, null, 2)}
152
+ ${delivery.note}
153
+ ` : '**No callbackUrl given** - payload only.'}
154
+
155
+ **Building the handler:**
156
+ 1. Answer **200** or payware retries - once a second, 15 times.
157
+ 2. Deduplicate on \`(transactionId, callbackType)\`; retries repeat the same pair.
158
+ 3. Read properties defensively: absent means absent, never zero.
159
+ 4. Treat \`EXPIRED\` as a non-payment, not an error to retry.
160
+ 5. Need the delivery address for a SHIPPABLE order? It is not here - call
161
+ \`payware_operations_get_transaction_history\`.`
162
+ }]
163
+ };
145
164
  }
146
- };
165
+ };