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