@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,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 history via payware API (completed/expired transactions)
|
|
@@ -21,7 +22,7 @@ export async function getPITransactionHistory({
|
|
|
21
22
|
throw new Error('Partner ID and private key are required for proper JWT creation');
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
// Create JWT token for GET request (no
|
|
25
|
+
// Create JWT token for GET request (no contentSha256 needed for GET)
|
|
25
26
|
const tokenData = createJWTToken(partnerId, privateKey, null);
|
|
26
27
|
|
|
27
28
|
// Required headers as per payware API documentation
|
|
@@ -44,16 +45,7 @@ export async function getPITransactionHistory({
|
|
|
44
45
|
timestamp: new Date().toISOString()
|
|
45
46
|
};
|
|
46
47
|
} catch (error) {
|
|
47
|
-
return
|
|
48
|
-
success: false,
|
|
49
|
-
error: {
|
|
50
|
-
message: error.response?.data?.message || error.message,
|
|
51
|
-
status: error.response?.status,
|
|
52
|
-
code: error.response?.data?.code,
|
|
53
|
-
details: error.response?.data
|
|
54
|
-
},
|
|
55
|
-
timestamp: new Date().toISOString()
|
|
56
|
-
};
|
|
48
|
+
return apiErrorResult(error);
|
|
57
49
|
}
|
|
58
50
|
}
|
|
59
51
|
|
|
@@ -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 via payware API (active transactions)
|
|
@@ -21,7 +22,7 @@ export async function getPITransactionStatus({
|
|
|
21
22
|
throw new Error('Partner ID and private key are required for proper JWT creation');
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
// Create JWT token for GET request (no
|
|
25
|
+
// Create JWT token for GET request (no contentSha256 needed for GET)
|
|
25
26
|
const tokenData = createJWTToken(partnerId, privateKey, null);
|
|
26
27
|
|
|
27
28
|
// Required headers as per payware API documentation
|
|
@@ -44,16 +45,7 @@ export async function getPITransactionStatus({
|
|
|
44
45
|
timestamp: new Date().toISOString()
|
|
45
46
|
};
|
|
46
47
|
} catch (error) {
|
|
47
|
-
return
|
|
48
|
-
success: false,
|
|
49
|
-
error: {
|
|
50
|
-
message: error.response?.data?.message || error.message,
|
|
51
|
-
status: error.response?.status,
|
|
52
|
-
code: error.response?.data?.code,
|
|
53
|
-
details: error.response?.data
|
|
54
|
-
},
|
|
55
|
-
timestamp: new Date().toISOString()
|
|
56
|
-
};
|
|
48
|
+
return apiErrorResult(error);
|
|
57
49
|
}
|
|
58
50
|
}
|
|
59
51
|
|
|
@@ -62,13 +54,14 @@ export async function getPITransactionStatus({
|
|
|
62
54
|
*/
|
|
63
55
|
export const getPITransactionStatusTool = {
|
|
64
56
|
name: "payware_operations_get_transaction_status",
|
|
65
|
-
description: `Get the current status of an active payware transaction as a
|
|
57
|
+
description: `Get the current status of an active payware transaction as a financial institution.
|
|
66
58
|
|
|
67
59
|
**Use Cases:**
|
|
68
60
|
- Check transaction status and details
|
|
69
61
|
- Monitor transaction progress
|
|
70
62
|
- Get participant information (payee/payer accounts, BICs)
|
|
71
63
|
- Check remaining time to live
|
|
64
|
+
- Fetch what to display to the payer for a product-originated transaction
|
|
72
65
|
|
|
73
66
|
**Response includes:**
|
|
74
67
|
- Transaction ID and type
|
|
@@ -77,6 +70,40 @@ export const getPITransactionStatusTool = {
|
|
|
77
70
|
- Reason lines and creation timestamp
|
|
78
71
|
- Remaining time to live
|
|
79
72
|
- Current transaction state
|
|
73
|
+
- \`product\` - payer-facing presentation, for product-originated transactions only (see below)
|
|
74
|
+
|
|
75
|
+
🛍️ **The nested \`product\` object.** Since 2026-07-22 the presentation fields are grouped under
|
|
76
|
+
\`product\` rather than sitting flat on the response - the old top-level \`imageUrl\` is gone.
|
|
77
|
+
Discriminate a product-originated transaction by the **presence of the object**, not by probing
|
|
78
|
+
individual fields. It carries: \`productId\`, \`type\`, \`name\`, \`shortDescription\`, \`longDescription\`,
|
|
79
|
+
\`imageUrl\`, \`imageDigest\`, \`imageContentType\`, \`imageBytes\`, \`termsUrl\`, \`termsText\`,
|
|
80
|
+
\`regularPrice\`, \`shippable\`, \`quickPay\`, \`shopName\`. Absent properties are omitted.
|
|
81
|
+
|
|
82
|
+
Four rules a payer-facing app must follow:
|
|
83
|
+
|
|
84
|
+
1. **Verify \`imageDigest\` before rendering.** It is the SHA-256 payware pinned for the image at
|
|
85
|
+
\`imageUrl\`. Fetch the image, check its bytes against this digest, and fall back to a neutral
|
|
86
|
+
placeholder on mismatch. Rendering an unverified image to a payer is how a compromised merchant
|
|
87
|
+
image host puts arbitrary content on a payment confirmation screen. \`imageContentType\` and
|
|
88
|
+
\`imageBytes\` are what payware observed on fetch - use them to reject a surprise.
|
|
89
|
+
2. **\`amount\` and \`currency\` at the top level are authoritative** - they are what the payer pays.
|
|
90
|
+
The \`product\` object does not repeat them. \`regularPrice\` is the pre-discount list price and is
|
|
91
|
+
present **only when it is higher than \`amount\`**, i.e. when there is a genuine saving to show; it
|
|
92
|
+
is omitted otherwise, precisely so no misleading comparison can be displayed.
|
|
93
|
+
3. **\`quickPay\` is advisory.** When true, the app may skip the product detail screen and go straight
|
|
94
|
+
to payment confirmation - but only if it still shows the payee name, amount and currency, and
|
|
95
|
+
still gives the payer access to the terms in full before they authorise.
|
|
96
|
+
4. **A missing field is usually a plan boundary, not an error.** \`imageUrl\` and \`termsUrl\` are
|
|
97
|
+
Standard and above; \`termsText\` and the descriptions are universal.
|
|
98
|
+
|
|
99
|
+
For a \`pr\`/\`ps\` identifier the values are resolved live (product -> shop -> merchant fallback, then
|
|
100
|
+
plan gating); for the resulting \`pw\` transaction they are the snapshot frozen at creation, so what
|
|
101
|
+
you get always reflects what the payer could have been shown at scan time.
|
|
102
|
+
|
|
103
|
+
💰 **Amounts follow the currency.** \`amount\`, \`fee\`, \`payerAmount\` and \`payeeAmount\` are decimal
|
|
104
|
+
strings at the smallest unit their own currency can be paid in - not always two decimals. \`fee\` is
|
|
105
|
+
**the value to echo back unchanged at finalize**; \`feeFixed\` and \`feeRate\` are informational, and
|
|
106
|
+
recomputing the fee from them is rejected with ERR_FEE_MISMATCH.
|
|
80
107
|
|
|
81
108
|
**Note:** This endpoint only returns active transactions. Use \`payware_pi_get_transaction_history\` for completed/expired transactions.`,
|
|
82
109
|
inputSchema: {
|
|
@@ -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 as a payment institution via payware API
|
|
@@ -73,10 +74,10 @@ export async function processPITransaction({
|
|
|
73
74
|
...(deliveryAddress && { deliveryAddress })
|
|
74
75
|
};
|
|
75
76
|
|
|
76
|
-
// Convert to deterministic minimized JSON for
|
|
77
|
+
// Convert to deterministic minimized JSON for SHA-256 calculation
|
|
77
78
|
const minimizedBodyString = createMinimizedJSON(requestBody);
|
|
78
79
|
|
|
79
|
-
// Create JWT token with
|
|
80
|
+
// Create JWT token with contentSha256 for the request body (POST requires contentSha256)
|
|
80
81
|
const tokenData = createJWTToken(partnerId, privateKey, minimizedBodyString);
|
|
81
82
|
|
|
82
83
|
// Required headers as per payware API documentation
|
|
@@ -88,7 +89,7 @@ export async function processPITransaction({
|
|
|
88
89
|
|
|
89
90
|
try {
|
|
90
91
|
const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
|
|
91
|
-
// Send the exact minimized JSON string that was used for
|
|
92
|
+
// Send the exact minimized JSON string that was used for SHA-256 calculation
|
|
92
93
|
const response = await axios.post(`${baseUrl}/transactions/${transactionId}`, minimizedBodyString, {
|
|
93
94
|
headers,
|
|
94
95
|
// Tell axios to send the string as-is, don't serialize it again
|
|
@@ -102,16 +103,7 @@ export async function processPITransaction({
|
|
|
102
103
|
timestamp: new Date().toISOString()
|
|
103
104
|
};
|
|
104
105
|
} catch (error) {
|
|
105
|
-
return
|
|
106
|
-
success: false,
|
|
107
|
-
error: {
|
|
108
|
-
message: error.response?.data?.message || error.message,
|
|
109
|
-
status: error.response?.status,
|
|
110
|
-
code: error.response?.data?.code,
|
|
111
|
-
details: error.response?.data
|
|
112
|
-
},
|
|
113
|
-
timestamp: new Date().toISOString()
|
|
114
|
-
};
|
|
106
|
+
return apiErrorResult(error);
|
|
115
107
|
}
|
|
116
108
|
}
|
|
117
109
|
|
|
@@ -30,10 +30,19 @@ export function generateMockPICallback(transactionId, callbackType = 'TRANSACTIO
|
|
|
30
30
|
paymentMethod = null,
|
|
31
31
|
status = 'CONFIRMED',
|
|
32
32
|
statusMessage = null,
|
|
33
|
-
passbackParams = null
|
|
33
|
+
passbackParams = null,
|
|
34
|
+
// The fee breakdown. Present on BOTH payloads whenever the transaction has a fee configuration -
|
|
35
|
+
// FINALIZED gained them on 2026-08-21, having previously carried the total without the two
|
|
36
|
+
// numbers that produced it. Both are INFORMATIONAL: recomputing the fee from them and submitting
|
|
37
|
+
// that at finalize is rejected with ERR_FEE_MISMATCH. Echo `fee` back unchanged instead.
|
|
38
|
+
feeFixed = '0.1000',
|
|
39
|
+
feeRate = '0.0150',
|
|
40
|
+
// Pass fee: null to model the anomaly the server reports by OMITTING the property - a
|
|
41
|
+
// transaction with no recorded fee. Deliberately distinguishable from a genuine '0.00'.
|
|
42
|
+
fee: feeOverride = undefined
|
|
34
43
|
} = options;
|
|
35
44
|
|
|
36
|
-
// Calculate fee like
|
|
45
|
+
// Calculate fee like the server: |payerAmount - amount| or |payeeAmount - amount|.
|
|
37
46
|
const transactionAmount = parseFloat(amount);
|
|
38
47
|
let fee = '0.60'; // Default fee
|
|
39
48
|
if (payerAmount !== null) {
|
|
@@ -41,15 +50,27 @@ export function generateMockPICallback(transactionId, callbackType = 'TRANSACTIO
|
|
|
41
50
|
} else if (payeeAmount !== null) {
|
|
42
51
|
fee = Math.abs(parseFloat(payeeAmount) - transactionAmount).toFixed(2);
|
|
43
52
|
}
|
|
53
|
+
if (feeOverride !== undefined) {
|
|
54
|
+
fee = feeOverride;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The server's payloads are @JsonInclude(NON_NULL): a property with no value is ABSENT from the
|
|
58
|
+
// JSON rather than sent as null. Emitting "passbackParams": null here taught integrators to expect
|
|
59
|
+
// a shape the server never sends.
|
|
60
|
+
const feeFields = {
|
|
61
|
+
...(fee !== null && fee !== undefined && { fee }),
|
|
62
|
+
...(feeFixed !== null && feeFixed !== undefined && { feeFixed }),
|
|
63
|
+
...(feeRate !== null && feeRate !== undefined && { feeRate })
|
|
64
|
+
};
|
|
44
65
|
|
|
45
66
|
// Build payload based on callback type (matching Java implementation exactly)
|
|
46
67
|
if (callbackType === 'TRANSACTION_PROCESSED') {
|
|
47
68
|
return {
|
|
48
69
|
callbackType,
|
|
49
70
|
transactionId,
|
|
50
|
-
passbackParams,
|
|
71
|
+
...(passbackParams !== null && passbackParams !== undefined && { passbackParams }),
|
|
51
72
|
amount,
|
|
52
|
-
|
|
73
|
+
...feeFields,
|
|
53
74
|
currency,
|
|
54
75
|
payeeBIC,
|
|
55
76
|
payeeAccount,
|
|
@@ -65,19 +86,24 @@ export function generateMockPICallback(transactionId, callbackType = 'TRANSACTIO
|
|
|
65
86
|
...(paymentMethod && { paymentMethod })
|
|
66
87
|
};
|
|
67
88
|
} else {
|
|
68
|
-
// TRANSACTION_FINALIZED
|
|
89
|
+
// TRANSACTION_FINALIZED.
|
|
90
|
+
//
|
|
91
|
+
// transactionType and initiatedBy are deliberately NOT here: the server's finalized builder
|
|
92
|
+
// never sets them, so @JsonInclude(NON_NULL) drops them. An institution that needs either for a
|
|
93
|
+
// finalized transaction reads it from GET /transactions-history/{transactionId}.
|
|
69
94
|
return {
|
|
70
95
|
callbackType,
|
|
71
96
|
transactionId,
|
|
72
|
-
passbackParams,
|
|
97
|
+
...(passbackParams !== null && passbackParams !== undefined && { passbackParams }),
|
|
73
98
|
amount,
|
|
74
|
-
|
|
99
|
+
...feeFields,
|
|
75
100
|
currency,
|
|
76
101
|
status,
|
|
77
102
|
...(statusMessage && { statusMessage }),
|
|
78
103
|
...(paymentMethod && { paymentMethod }),
|
|
79
104
|
created: Date.now() - 300000, // Created 5 minutes ago (milliseconds)
|
|
80
|
-
|
|
105
|
+
// Absent on EXPIRED: the expiry sweep transitions status in bulk and sets no finalized moment.
|
|
106
|
+
...(status !== 'EXPIRED' && { finalized: Date.now() })
|
|
81
107
|
};
|
|
82
108
|
}
|
|
83
109
|
}
|
|
@@ -152,6 +178,16 @@ export const simulatePICallbackTool = {
|
|
|
152
178
|
description: "Fee amount charged by payment institution (e.g., '0.60')",
|
|
153
179
|
default: "0.60"
|
|
154
180
|
},
|
|
181
|
+
feeFixed: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "Fixed fee component of the applicable fee configuration. Informational, for reconciliation - do not recompute the fee from it. Present on both callback types since 2026-08-21. Pass null to model a transaction with no fee configuration, where it is omitted.",
|
|
184
|
+
default: "0.1000"
|
|
185
|
+
},
|
|
186
|
+
feeRate: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description: "Variable rate of the applicable fee configuration, e.g. '0.0150' = 1.5%. Informational, same as feeFixed.",
|
|
189
|
+
default: "0.0150"
|
|
190
|
+
},
|
|
155
191
|
payeeBIC: {
|
|
156
192
|
type: "string",
|
|
157
193
|
description: "Payee's BIC code",
|
|
@@ -222,7 +258,9 @@ export const simulatePICallbackTool = {
|
|
|
222
258
|
callbackUrl,
|
|
223
259
|
amount = '57.00',
|
|
224
260
|
currency = 'EUR',
|
|
225
|
-
fee
|
|
261
|
+
fee,
|
|
262
|
+
feeFixed = '0.1000',
|
|
263
|
+
feeRate = '0.0150',
|
|
226
264
|
payeeBIC = 'SBBICID1',
|
|
227
265
|
payeeAccount = 'M-1W-1account2',
|
|
228
266
|
payeeFriendlyName = 'payware',
|
|
@@ -246,6 +284,8 @@ export const simulatePICallbackTool = {
|
|
|
246
284
|
amount,
|
|
247
285
|
currency,
|
|
248
286
|
fee,
|
|
287
|
+
feeFixed,
|
|
288
|
+
feeRate,
|
|
249
289
|
payeeBIC,
|
|
250
290
|
payeeAccount,
|
|
251
291
|
payeeFriendlyName,
|
|
@@ -3,6 +3,7 @@ import FormData from 'form-data';
|
|
|
3
3
|
import { createJWTToken } from '../../core/auth/jwt-token.js';
|
|
4
4
|
import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
|
|
5
5
|
import { readFileSync } from 'fs';
|
|
6
|
+
import { apiErrorResult } from '../../shared/api-errors.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Get transaction information using Soundbite file via payware API
|
|
@@ -37,8 +38,8 @@ export async function getSoundbiteTransaction({
|
|
|
37
38
|
contentType: 'application/octet-stream'
|
|
38
39
|
});
|
|
39
40
|
|
|
40
|
-
// For multipart/form-data requests, we don't calculate
|
|
41
|
-
// The JWT token is created without content
|
|
41
|
+
// For multipart/form-data requests, we don't calculate contentSha256 the same way
|
|
42
|
+
// The JWT token is created without content SHA-256 for multipart uploads
|
|
42
43
|
const tokenData = createJWTToken(partnerId, privateKey, null);
|
|
43
44
|
|
|
44
45
|
// Required headers as per payware API documentation
|
|
@@ -63,16 +64,7 @@ export async function getSoundbiteTransaction({
|
|
|
63
64
|
timestamp: new Date().toISOString()
|
|
64
65
|
};
|
|
65
66
|
} catch (error) {
|
|
66
|
-
return
|
|
67
|
-
success: false,
|
|
68
|
-
error: {
|
|
69
|
-
message: error.response?.data?.message || error.message,
|
|
70
|
-
status: error.response?.status,
|
|
71
|
-
code: error.response?.data?.code,
|
|
72
|
-
details: error.response?.data
|
|
73
|
-
},
|
|
74
|
-
timestamp: new Date().toISOString()
|
|
75
|
-
};
|
|
67
|
+
return apiErrorResult(error);
|
|
76
68
|
}
|
|
77
69
|
}
|
|
78
70
|
|
|
@@ -98,7 +90,7 @@ export const soundbiteTransactionTool = {
|
|
|
98
90
|
**API Request:**
|
|
99
91
|
- Uses multipart/form-data content type
|
|
100
92
|
- File uploaded as form field named 'file'
|
|
101
|
-
- JWT authentication without
|
|
93
|
+
- JWT authentication without contentSha256 (multipart upload)`,
|
|
102
94
|
inputSchema: {
|
|
103
95
|
type: "object",
|
|
104
96
|
properties: {
|
package/src/proxy-server.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place that turns an axios failure from the payware API into something an assistant can act on.
|
|
3
|
+
*
|
|
4
|
+
* Every module here used to do the same thing inline:
|
|
5
|
+
*
|
|
6
|
+
* throw new Error(`Failed to get products: ${error.response?.data?.message || error.message}`);
|
|
7
|
+
*
|
|
8
|
+
* which keeps the server's sentence and throws away the two things that decide what to do next: the
|
|
9
|
+
* HTTP status and the response headers. That is survivable for a 400 - the message names the bad
|
|
10
|
+
* field - and it is not survivable for the statuses that are reachable on EVERY endpoint from
|
|
11
|
+
* machinery no individual handler knows about:
|
|
12
|
+
*
|
|
13
|
+
* - **429** is refused by `PartnerApiRateLimitFilter` before any handler runs, and carries a
|
|
14
|
+
* `Retry-After` header. Flattened to a string, "Rate limit exceeded. Please slow down your
|
|
15
|
+
* requests." reads like advice rather than like a number of seconds to wait, and the header -
|
|
16
|
+
* the only part that says how long - is gone.
|
|
17
|
+
* - **403** on an ISV call is nearly always the acting merchant's plan, not the ISV's own
|
|
18
|
+
* permissions. Without that hint the obvious next move is to re-check credentials that were
|
|
19
|
+
* never the problem.
|
|
20
|
+
* - **409** and **503** both mean "the request was not applied", i.e. retryable, and a bare
|
|
21
|
+
* message makes them indistinguishable from a 400, which is not.
|
|
22
|
+
*
|
|
23
|
+
* So this returns an Error whose message states the status, the payware error code, what it means,
|
|
24
|
+
* and whether retrying is correct.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Statuses reachable on any endpoint, with what a caller should actually do about each. */
|
|
28
|
+
const UNIVERSAL_STATUS_GUIDANCE = {
|
|
29
|
+
401: {
|
|
30
|
+
label: 'Unauthenticated',
|
|
31
|
+
guidance:
|
|
32
|
+
'The JWT was missing, malformed, expired, or signed with a key that does not match the ' +
|
|
33
|
+
'partnerId. Check that the private key in keys/ belongs to the environment being called - a ' +
|
|
34
|
+
'sandbox key against production authenticates as nobody. Not retryable as-is.'
|
|
35
|
+
},
|
|
36
|
+
403: {
|
|
37
|
+
label: 'Forbidden',
|
|
38
|
+
guidance:
|
|
39
|
+
'Authenticated, but not permitted. On an ISV on-behalf call this is usually the MERCHANT\'s ' +
|
|
40
|
+
'plan rather than the ISV\'s own permissions - the operating plan is the merchant\'s, and ' +
|
|
41
|
+
'Basic merchants are outside the transaction, product, shop and report surfaces. Other ' +
|
|
42
|
+
'causes: the shop is not in the ISV\'s assigned scope (ERR_SHOP_NOT_IN_SCOPE), the report is ' +
|
|
43
|
+
'not available to this plan or partner type (ERR_REPORT_NOT_AVAILABLE), or a KYC gate. Not ' +
|
|
44
|
+
'retryable without changing the plan, the scope, or the resource.'
|
|
45
|
+
},
|
|
46
|
+
409: {
|
|
47
|
+
label: 'Conflict',
|
|
48
|
+
guidance:
|
|
49
|
+
'The request was well formed and conflicts with the current state - most often ' +
|
|
50
|
+
'ERR_ALREADY_PROCESSED, which is the answer to submitting the same transaction twice. Treat ' +
|
|
51
|
+
'it as "already done" rather than as a failure: re-read the transaction before retrying, or ' +
|
|
52
|
+
'the retry will conflict again.'
|
|
53
|
+
},
|
|
54
|
+
429: {
|
|
55
|
+
label: 'Rate limited',
|
|
56
|
+
guidance:
|
|
57
|
+
'The per-partner rate limit was exceeded and the request was NOT processed - nothing was ' +
|
|
58
|
+
'created, updated or charged, so the identical request is safe to retry. Wait for the ' +
|
|
59
|
+
'Retry-After value below before retrying, and add jitter if more than one worker is running, ' +
|
|
60
|
+
'so that the retries do not re-synchronise into a second burst.'
|
|
61
|
+
},
|
|
62
|
+
503: {
|
|
63
|
+
label: 'Service unavailable',
|
|
64
|
+
guidance:
|
|
65
|
+
'A dependency payware needs was unavailable - typically no fee configuration for the ' +
|
|
66
|
+
'transaction currency (ERR_FEE_CONFIGURATION_UNAVAILABLE) or no exchange rate ' +
|
|
67
|
+
'(ERR_MISSING_EXCHANGE_RATE). The caller did nothing wrong and the request was not applied, ' +
|
|
68
|
+
'so retrying later is correct. If it persists, it is a payware-side configuration gap - ' +
|
|
69
|
+
'report it rather than retrying indefinitely.'
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether retrying the identical request is safe and meaningful.
|
|
75
|
+
*
|
|
76
|
+
* Deliberately narrow. 429 and 503 are the two statuses that state the request was not applied;
|
|
77
|
+
* everything else either succeeded, conflicts, or needs the request changed first.
|
|
78
|
+
*/
|
|
79
|
+
export function isRetryable(status) {
|
|
80
|
+
return status === 429 || status === 503;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Seconds to wait before retrying, read from the response's Retry-After header.
|
|
85
|
+
*
|
|
86
|
+
* Returns null when the header is absent or is an HTTP-date rather than a delta-seconds value.
|
|
87
|
+
* payware sends delta-seconds, but a proxy in front of it may not, and guessing a date's meaning is
|
|
88
|
+
* worse than saying nothing.
|
|
89
|
+
*/
|
|
90
|
+
export function retryAfterSeconds(error) {
|
|
91
|
+
const raw = error?.response?.headers?.['retry-after'] ?? error?.response?.headers?.['Retry-After'];
|
|
92
|
+
if (raw === undefined || raw === null) return null;
|
|
93
|
+
const seconds = Number.parseInt(String(raw).trim(), 10);
|
|
94
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the Error to throw for a failed payware API call.
|
|
99
|
+
*
|
|
100
|
+
* @param {Error} error the axios error
|
|
101
|
+
* @param {string} operation what was being attempted, e.g. 'get products' - phrased to follow "Failed to "
|
|
102
|
+
* @returns {Error} an Error carrying `status`, `errorCode`, `retryable` and `retryAfterSeconds`
|
|
103
|
+
* alongside a message that explains the status
|
|
104
|
+
*/
|
|
105
|
+
export function describeApiError(error, operation) {
|
|
106
|
+
const status = error?.response?.status;
|
|
107
|
+
const data = error?.response?.data;
|
|
108
|
+
const errorCode = data?.errorCode || data?.code;
|
|
109
|
+
const serverMessage = data?.message || error?.message;
|
|
110
|
+
const correlationId = data?.correlationId;
|
|
111
|
+
|
|
112
|
+
const parts = [`Failed to ${operation}`];
|
|
113
|
+
const known = UNIVERSAL_STATUS_GUIDANCE[status];
|
|
114
|
+
if (status) {
|
|
115
|
+
parts.push(`: HTTP ${status}${known ? ` (${known.label})` : ''}`);
|
|
116
|
+
} else {
|
|
117
|
+
parts.push(':');
|
|
118
|
+
}
|
|
119
|
+
if (errorCode) parts.push(` [${errorCode}]`);
|
|
120
|
+
if (serverMessage) parts.push(` ${serverMessage}`);
|
|
121
|
+
|
|
122
|
+
const detail = [];
|
|
123
|
+
if (known) detail.push(known.guidance);
|
|
124
|
+
|
|
125
|
+
const retryAfter = retryAfterSeconds(error);
|
|
126
|
+
if (retryAfter !== null) {
|
|
127
|
+
detail.push(`Retry-After: ${retryAfter} seconds.`);
|
|
128
|
+
} else if (status === 429) {
|
|
129
|
+
// The filter always sets it, so its absence means something sat between us and payware.
|
|
130
|
+
detail.push('No Retry-After header was returned; back off at least 60 seconds.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The single most useful thing to quote when reporting a problem to payware support, and it is
|
|
134
|
+
// dropped entirely by a message-only error.
|
|
135
|
+
if (correlationId) detail.push(`correlationId: ${correlationId} - quote this to payware support.`);
|
|
136
|
+
|
|
137
|
+
const message = parts.join('') + (detail.length ? `\n\n${detail.join('\n')}` : '');
|
|
138
|
+
|
|
139
|
+
const wrapped = new Error(message);
|
|
140
|
+
wrapped.status = status;
|
|
141
|
+
wrapped.errorCode = errorCode;
|
|
142
|
+
wrapped.correlationId = correlationId;
|
|
143
|
+
wrapped.retryable = isRetryable(status);
|
|
144
|
+
wrapped.retryAfterSeconds = retryAfter;
|
|
145
|
+
wrapped.cause = error;
|
|
146
|
+
return wrapped;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The `{ success: false, error: {...} }` shape the transaction and POI modules return instead of
|
|
151
|
+
* throwing, built from one place.
|
|
152
|
+
*
|
|
153
|
+
* **The bug this exists to stop repeating.** Those modules each built the shape inline and read the
|
|
154
|
+
* payware error code as `error.response?.data?.code`. payware does not send a `code` property. Its
|
|
155
|
+
* error body is `{ errorCode, message, correlationId }` - `ApiError.errorCode`, with no
|
|
156
|
+
* `@JsonProperty` renaming it - so `.code` was `undefined` on every payware error in every one of
|
|
157
|
+
* those modules. Nothing failed loudly: the message still came through, so the shape looked healthy
|
|
158
|
+
* while the machine-readable half of it was always empty. Anything branching on the code silently
|
|
159
|
+
* took the else branch forever - `create-transaction.js` had exactly that, a hash-mismatch help text
|
|
160
|
+
* gated on `data.code === 'ERR_INVALID_CONTENT_HASH'` that could never fire.
|
|
161
|
+
*
|
|
162
|
+
* `code` is still populated below, as an alias of `errorCode`, so that any caller still reading the
|
|
163
|
+
* old property keeps working - it just now has a value.
|
|
164
|
+
*/
|
|
165
|
+
export function apiErrorResult(error) {
|
|
166
|
+
const status = error?.response?.status;
|
|
167
|
+
const data = error?.response?.data;
|
|
168
|
+
const errorCode = data?.errorCode || data?.code;
|
|
169
|
+
const retryAfter = retryAfterSeconds(error);
|
|
170
|
+
const known = UNIVERSAL_STATUS_GUIDANCE[status];
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: {
|
|
175
|
+
message: data?.message || error?.message,
|
|
176
|
+
status,
|
|
177
|
+
errorCode,
|
|
178
|
+
// Alias, kept populated for backward compatibility - see the note above.
|
|
179
|
+
code: errorCode,
|
|
180
|
+
correlationId: data?.correlationId,
|
|
181
|
+
retryable: isRetryable(status),
|
|
182
|
+
...(retryAfter !== null && { retryAfterSeconds: retryAfter }),
|
|
183
|
+
...(known && { guidance: known.guidance }),
|
|
184
|
+
details: data
|
|
185
|
+
},
|
|
186
|
+
timestamp: new Date().toISOString()
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createJWTToken } from '../core/auth/jwt-token.js';
|
|
2
|
+
import { createJWTForPartner } from '../core/auth/jwt-factory.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the Authorization headers for a payware API call, for any partner role.
|
|
6
|
+
*
|
|
7
|
+
* Merchants and financial institutions sign with their own key. An ISV signs with its own key too,
|
|
8
|
+
* but adds the merchant it is acting for in the `aud` claim and that merchant's OAuth2 token in
|
|
9
|
+
* `sub` - which is what makes the call an on-behalf call rather than an ISV-for-itself call.
|
|
10
|
+
*
|
|
11
|
+
* **Note on duplication.** Four near-identical private copies of this function already exist, in
|
|
12
|
+
* `shared/products/products-api.js`, `shared/data/data-api.js`, `shared/deep-links/api.js` and
|
|
13
|
+
* `shared/logs/logs-api.js`. This is the shared one, used by everything added since. The four were
|
|
14
|
+
* deliberately left alone rather than swept into it: they are on live, working paths, they differ in
|
|
15
|
+
* small ways that look accidental but have not been proven so (deep-links defaults `requestBody` to
|
|
16
|
+
* `''` where the others pass `undefined`, and for a GET those are not always the same thing to a
|
|
17
|
+
* hash calculation), and consolidating them is a refactor of authentication with no test suite
|
|
18
|
+
* behind it. Do it as its own change, with the `''`-vs-`undefined` question answered first, not as a
|
|
19
|
+
* side effect of adding a tool.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} partnerType 'merchant' | 'payment_institution' | 'isv'
|
|
22
|
+
* @param {Object} authParams partnerId + privateKey, or isvPartnerId + privateKey +
|
|
23
|
+
* merchantPartnerId + oauth2Token; plus requestBody for writes
|
|
24
|
+
* @returns {Promise<Object>} headers including Authorization, Content-Type and Api-Version
|
|
25
|
+
*/
|
|
26
|
+
export async function createAuthHeaders(partnerType, authParams) {
|
|
27
|
+
const baseHeaders = {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Api-Version': '1'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let tokenData;
|
|
33
|
+
|
|
34
|
+
switch (partnerType) {
|
|
35
|
+
case 'merchant':
|
|
36
|
+
case 'payment_institution':
|
|
37
|
+
tokenData = createJWTToken(
|
|
38
|
+
authParams.partnerId,
|
|
39
|
+
authParams.privateKey,
|
|
40
|
+
authParams.requestBody
|
|
41
|
+
);
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case 'isv':
|
|
45
|
+
tokenData = await createJWTForPartner({
|
|
46
|
+
partnerId: authParams.isvPartnerId,
|
|
47
|
+
privateKey: authParams.privateKey,
|
|
48
|
+
merchantId: authParams.merchantPartnerId,
|
|
49
|
+
oauth2Token: authParams.oauth2Token,
|
|
50
|
+
requestBody: authParams.requestBody
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`Unknown partner type: ${partnerType}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...baseHeaders,
|
|
60
|
+
'Authorization': `Bearer ${tokenData.token}`
|
|
61
|
+
};
|
|
62
|
+
}
|