@payware/mcp-server 1.2.0 → 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/README.md +5 -0
- package/package.json +69 -66
- package/src/auth/sandbox-setup.js +186 -186
- package/src/isv/index.js +29 -1
- package/src/isv/invitations/index.js +471 -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/create-product.js +313 -313
- 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 +41 -10
- package/src/isv/transactions/create-transaction.js +20 -4
- 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/merchant/data/generate-report.js +19 -1
- package/src/merchant/index.js +21 -1
- package/src/merchant/logs/index.js +366 -0
- package/src/merchant/products/create-product.js +287 -287
- package/src/merchant/products/update-product.js +281 -281
- package/src/merchant/shops/index.js +18 -0
- package/src/merchant/transactions/cancel-transaction.js +10 -13
- package/src/merchant/transactions/create-transaction.js +88 -8
- package/src/merchant/transactions/get-status.js +40 -11
- package/src/merchant/transactions/process-transaction.js +2 -10
- package/src/merchant/transactions/simulate-callback.js +68 -9
- package/src/merchant/transactions/transaction-history.js +275 -257
- package/src/payment-institution/transactions/create-pi-transaction.js +2 -10
- package/src/payment-institution/transactions/finalize-transaction.js +333 -340
- package/src/payment-institution/transactions/get-transaction-history.js +2 -10
- package/src/payment-institution/transactions/get-transaction-status.js +38 -11
- package/src/payment-institution/transactions/process-pi-transaction.js +2 -10
- package/src/payment-institution/transactions/simulate-pi-callback.js +49 -9
- package/src/payment-institution/transactions/soundbite-transaction.js +2 -10
- 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/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/isv/oauth2.js +458 -600
- package/src/utils/code-examples/isv/transactions.js +815 -812
- 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)
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -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
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createJWTToken } from '../../core/auth/jwt-token.js';
|
|
|
3
3
|
import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
|
|
4
4
|
import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
|
|
5
5
|
import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
|
|
6
|
+
import { describeApiError } from '../api-errors.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Available report types from the Data API documentation
|
|
@@ -147,7 +148,7 @@ export async function generateReport({
|
|
|
147
148
|
}
|
|
148
149
|
};
|
|
149
150
|
} catch (error) {
|
|
150
|
-
throw
|
|
151
|
+
throw describeApiError(error, 'generate report');
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
@@ -181,7 +182,7 @@ export async function getReportStatus({
|
|
|
181
182
|
}
|
|
182
183
|
};
|
|
183
184
|
} catch (error) {
|
|
184
|
-
throw
|
|
185
|
+
throw describeApiError(error, 'get report status');
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -222,7 +223,7 @@ export async function exportReport({
|
|
|
222
223
|
}
|
|
223
224
|
};
|
|
224
225
|
} catch (error) {
|
|
225
|
-
throw
|
|
226
|
+
throw describeApiError(error, 'export report');
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
|
|
@@ -256,7 +257,7 @@ export async function getExportStatus({
|
|
|
256
257
|
}
|
|
257
258
|
};
|
|
258
259
|
} catch (error) {
|
|
259
|
-
throw
|
|
260
|
+
throw describeApiError(error, 'get export status');
|
|
260
261
|
}
|
|
261
262
|
}
|
|
262
263
|
|
|
@@ -295,7 +296,7 @@ export async function downloadExport({
|
|
|
295
296
|
}
|
|
296
297
|
};
|
|
297
298
|
} catch (error) {
|
|
298
|
-
throw
|
|
299
|
+
throw describeApiError(error, 'download export');
|
|
299
300
|
}
|
|
300
301
|
}
|
|
301
302
|
|
|
@@ -331,7 +332,7 @@ export async function cancelReport({
|
|
|
331
332
|
}
|
|
332
333
|
};
|
|
333
334
|
} catch (error) {
|
|
334
|
-
throw
|
|
335
|
+
throw describeApiError(error, 'cancel report');
|
|
335
336
|
}
|
|
336
337
|
}
|
|
337
338
|
|
|
@@ -367,7 +368,7 @@ export async function cancelExport({
|
|
|
367
368
|
}
|
|
368
369
|
};
|
|
369
370
|
} catch (error) {
|
|
370
|
-
throw
|
|
371
|
+
throw describeApiError(error, 'cancel export');
|
|
371
372
|
}
|
|
372
373
|
}
|
|
373
374
|
|
|
@@ -396,7 +397,7 @@ export async function listReports({
|
|
|
396
397
|
}
|
|
397
398
|
};
|
|
398
399
|
} catch (error) {
|
|
399
|
-
throw
|
|
400
|
+
throw describeApiError(error, 'list reports');
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
|
|
@@ -430,7 +431,7 @@ export async function getReportRequests({
|
|
|
430
431
|
}
|
|
431
432
|
};
|
|
432
433
|
} catch (error) {
|
|
433
|
-
throw
|
|
434
|
+
throw describeApiError(error, 'get report requests');
|
|
434
435
|
}
|
|
435
436
|
}
|
|
436
437
|
|
|
@@ -464,6 +465,6 @@ export async function getExportList({
|
|
|
464
465
|
}
|
|
465
466
|
};
|
|
466
467
|
} catch (error) {
|
|
467
|
-
throw
|
|
468
|
+
throw describeApiError(error, 'get export list');
|
|
468
469
|
}
|
|
469
470
|
}
|
|
@@ -2,6 +2,7 @@ import axios from 'axios';
|
|
|
2
2
|
import { createJWTToken } from '../../core/auth/jwt-token.js';
|
|
3
3
|
import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
|
|
4
4
|
import { getProductionUrl } from '../../config/env.js';
|
|
5
|
+
import { describeApiError } from '../api-errors.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Create authentication headers based on partner type
|
|
@@ -84,7 +85,7 @@ export async function getTransactionDeepLink({
|
|
|
84
85
|
}
|
|
85
86
|
};
|
|
86
87
|
} catch (error) {
|
|
87
|
-
throw
|
|
88
|
+
throw describeApiError(error, 'get transaction deep link');
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -127,7 +128,7 @@ export async function getProductDeepLink({
|
|
|
127
128
|
}
|
|
128
129
|
};
|
|
129
130
|
} catch (error) {
|
|
130
|
-
throw
|
|
131
|
+
throw describeApiError(error, 'get product deep link');
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -166,7 +167,7 @@ export async function deleteTransactionDeepLink({
|
|
|
166
167
|
}
|
|
167
168
|
};
|
|
168
169
|
} catch (error) {
|
|
169
|
-
throw
|
|
170
|
+
throw describeApiError(error, 'delete transaction deep link');
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -209,6 +210,6 @@ export async function deleteProductDeepLink({
|
|
|
209
210
|
}
|
|
210
211
|
};
|
|
211
212
|
} catch (error) {
|
|
212
|
-
throw
|
|
213
|
+
throw describeApiError(error, 'delete product deep link');
|
|
213
214
|
}
|
|
214
215
|
}
|