@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.
- package/.env.example +0 -0
- package/README.md +22 -16
- package/check-keys.js +0 -0
- package/keys/README.md +0 -0
- package/mcp-config.json +0 -0
- package/package.json +69 -66
- package/payware-mcp-config.json +0 -0
- package/src/auth/rsa-keys.js +0 -0
- package/src/auth/sandbox-setup.js +186 -186
- package/src/config/env.js +0 -0
- package/src/config/partner-types.js +0 -0
- package/src/core/auth/jwt-factory.js +4 -4
- package/src/core/auth/jwt-token.js +107 -66
- package/src/core/utils/json-serializer.js +5 -5
- package/src/core/utils/key-utils.js +0 -0
- package/src/index.js +0 -0
- package/src/isv/data/index.js +0 -0
- package/src/isv/deep-links/delete-product-link.js +0 -0
- package/src/isv/deep-links/delete-transaction-link.js +0 -0
- package/src/isv/deep-links/get-product-link.js +0 -0
- package/src/isv/deep-links/get-transaction-link.js +0 -0
- package/src/isv/deep-links/index.js +0 -0
- package/src/isv/index.js +29 -1
- package/src/isv/invitations/index.js +471 -0
- package/src/isv/oauth2/create-token-simple.js +0 -0
- package/src/isv/oauth2/get-token-info.js +0 -0
- package/src/isv/oauth2/obtain-token.js +19 -8
- package/src/isv/oauth2/token-helper.js +5 -2
- package/src/isv/poi/cancel-price.js +2 -10
- package/src/isv/poi/create.js +19 -0
- package/src/isv/poi/get-poi.js +2 -10
- package/src/isv/poi/get-qrcode.js +2 -10
- package/src/isv/poi/get-status.js +2 -10
- package/src/isv/poi/index.js +5 -0
- package/src/isv/poi/list-pois.js +2 -10
- package/src/isv/poi/set-price.js +217 -225
- package/src/isv/products/audios.js +0 -0
- package/src/isv/products/create-product.js +313 -313
- package/src/isv/products/delete-product.js +0 -0
- package/src/isv/products/get-product-image.js +0 -0
- package/src/isv/products/get-product.js +0 -0
- package/src/isv/products/index.js +0 -0
- package/src/isv/products/list-products.js +0 -0
- package/src/isv/products/schedules.js +0 -0
- package/src/isv/products/update-product.js +264 -264
- package/src/isv/reference/index.js +249 -0
- package/src/isv/shops/index.js +22 -0
- package/src/isv/transactions/cancel-transaction.js +43 -12
- package/src/isv/transactions/create-transaction.js +22 -6
- package/src/isv/transactions/get-status.js +18 -11
- package/src/isv/transactions/process-transaction.js +160 -42
- package/src/isv/transactions/simulate-callback.js +128 -109
- package/src/isv/transactions/transaction-history.js +86 -90
- package/src/isv/utils/auth.js +2 -2
- package/src/mcp-proxy.js +0 -0
- package/src/merchant/data/export-report.js +0 -0
- package/src/merchant/data/generate-report.js +19 -1
- package/src/merchant/data/get-export-list.js +0 -0
- package/src/merchant/data/get-report-requests.js +0 -0
- package/src/merchant/data/get-report-status.js +0 -0
- package/src/merchant/data/index.js +0 -0
- package/src/merchant/data/list-reports.js +0 -0
- package/src/merchant/deep-links/delete-product-link.js +0 -0
- package/src/merchant/deep-links/delete-transaction-link.js +0 -0
- package/src/merchant/deep-links/get-product-link.js +0 -0
- package/src/merchant/deep-links/get-transaction-link.js +0 -0
- package/src/merchant/deep-links/index.js +0 -0
- package/src/merchant/index.js +21 -1
- package/src/merchant/logs/index.js +366 -0
- package/src/merchant/products/audios.js +0 -0
- package/src/merchant/products/create-product.js +287 -287
- package/src/merchant/products/delete-product.js +0 -0
- package/src/merchant/products/get-product-image.js +0 -0
- package/src/merchant/products/get-product.js +0 -0
- package/src/merchant/products/index.js +0 -0
- package/src/merchant/products/list-products.js +0 -0
- package/src/merchant/products/schedules.js +0 -0
- package/src/merchant/products/update-product.js +281 -281
- package/src/merchant/shops/index.js +18 -0
- package/src/merchant/transactions/cancel-transaction.js +16 -19
- package/src/merchant/transactions/create-transaction.js +99 -19
- package/src/merchant/transactions/get-status.js +41 -12
- package/src/merchant/transactions/process-transaction.js +8 -16
- package/src/merchant/transactions/simulate-callback.js +68 -9
- package/src/merchant/transactions/transaction-history.js +275 -257
- package/src/payment-institution/data/index.js +0 -0
- package/src/payment-institution/deep-links/delete-transaction-link.js +0 -0
- package/src/payment-institution/deep-links/get-transaction-link.js +0 -0
- package/src/payment-institution/deep-links/index.js +0 -0
- package/src/payment-institution/index.js +0 -0
- package/src/payment-institution/transactions/create-pi-transaction.js +6 -14
- package/src/payment-institution/transactions/finalize-transaction.js +333 -340
- package/src/payment-institution/transactions/get-transaction-history.js +3 -11
- package/src/payment-institution/transactions/get-transaction-status.js +39 -12
- package/src/payment-institution/transactions/process-pi-transaction.js +5 -13
- package/src/payment-institution/transactions/simulate-pi-callback.js +49 -9
- package/src/payment-institution/transactions/soundbite-transaction.js +5 -13
- package/src/proxy-server.js +0 -0
- package/src/shared/api-errors.js +188 -0
- package/src/shared/auth-headers.js +62 -0
- package/src/shared/data/data-api.js +11 -10
- package/src/shared/deep-links/api.js +5 -4
- package/src/shared/index.js +0 -0
- package/src/shared/logs/logs-api.js +242 -0
- package/src/shared/poi/poi-create-api.js +419 -0
- package/src/shared/products/products-api.js +17 -16
- package/src/shared/shops/shops-api.js +155 -0
- package/src/tools/advanced-code-generator.js +2255 -2246
- package/src/utils/code-examples/common/auth-examples.js +1484 -1481
- package/src/utils/code-examples/common/helpers.js +7 -4
- package/src/utils/code-examples/common/utils-examples.js +0 -0
- package/src/utils/code-examples/frameworks/examples.js +4 -4
- package/src/utils/code-examples/index.js +0 -0
- package/src/utils/code-examples/isv/oauth2.js +458 -600
- package/src/utils/code-examples/isv/products.js +8 -8
- package/src/utils/code-examples/isv/transactions.js +815 -812
- package/src/utils/code-examples/merchant/products.js +0 -0
- package/src/utils/code-examples/merchant/transactions.js +9 -9
- package/src/utils/code-examples/payment-institution/transactions.js +0 -0
- package/src/utils/code-examples/shared/data.js +0 -0
- package/src/utils/code-examples/shared/deep-links.js +0 -0
- package/src/utils/code-examples/shared/soundbites.js +0 -0
- package/src/utils/code-examples.js +24 -24
- package/src/utils/format-request.js +3 -3
- package/src/utils/json-formatter.js +20 -20
- package/src/utils/server-info.js +0 -0
- 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
|
|
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({
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
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: '
|
|
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:
|
|
230
|
+
text: `✅ **Transaction Processed** (ISV -> Merchant: ${params.merchantPartnerId})
|
|
111
231
|
|
|
112
|
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
|
|
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
|
-
-
|
|
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
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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'
|
|
51
|
+
required: ['transactionId'],
|
|
74
52
|
properties: {
|
|
75
53
|
transactionId: {
|
|
76
54
|
type: 'string',
|
|
77
|
-
description: 'Transaction ID to
|
|
55
|
+
description: 'Transaction ID to build the callback payload for'
|
|
78
56
|
},
|
|
79
|
-
|
|
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: '
|
|
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
|
-
|
|
68
|
+
amount: {
|
|
84
69
|
type: 'string',
|
|
85
|
-
description:
|
|
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
|
-
|
|
73
|
+
currency: {
|
|
88
74
|
type: 'string',
|
|
89
|
-
enum: ['
|
|
90
|
-
|
|
91
|
-
|
|
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: '
|
|
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(
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
133
|
+
const payload = generateMockCallback(transactionId, status, {
|
|
134
|
+
amount, currency, fee, feeFixed, feeRate, statusMessage, paymentMethod
|
|
135
|
+
});
|
|
106
136
|
|
|
107
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
};
|