@payware/mcp-server 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.env.example +0 -0
  2. package/README.md +22 -16
  3. package/check-keys.js +0 -0
  4. package/keys/README.md +0 -0
  5. package/mcp-config.json +0 -0
  6. package/package.json +69 -66
  7. package/payware-mcp-config.json +0 -0
  8. package/src/auth/rsa-keys.js +0 -0
  9. package/src/auth/sandbox-setup.js +186 -186
  10. package/src/config/env.js +0 -0
  11. package/src/config/partner-types.js +0 -0
  12. package/src/core/auth/jwt-factory.js +4 -4
  13. package/src/core/auth/jwt-token.js +107 -66
  14. package/src/core/utils/json-serializer.js +5 -5
  15. package/src/core/utils/key-utils.js +0 -0
  16. package/src/index.js +0 -0
  17. package/src/isv/data/index.js +0 -0
  18. package/src/isv/deep-links/delete-product-link.js +0 -0
  19. package/src/isv/deep-links/delete-transaction-link.js +0 -0
  20. package/src/isv/deep-links/get-product-link.js +0 -0
  21. package/src/isv/deep-links/get-transaction-link.js +0 -0
  22. package/src/isv/deep-links/index.js +0 -0
  23. package/src/isv/index.js +29 -1
  24. package/src/isv/invitations/index.js +471 -0
  25. package/src/isv/oauth2/create-token-simple.js +0 -0
  26. package/src/isv/oauth2/get-token-info.js +0 -0
  27. package/src/isv/oauth2/obtain-token.js +19 -8
  28. package/src/isv/oauth2/token-helper.js +5 -2
  29. package/src/isv/poi/cancel-price.js +2 -10
  30. package/src/isv/poi/create.js +19 -0
  31. package/src/isv/poi/get-poi.js +2 -10
  32. package/src/isv/poi/get-qrcode.js +2 -10
  33. package/src/isv/poi/get-status.js +2 -10
  34. package/src/isv/poi/index.js +5 -0
  35. package/src/isv/poi/list-pois.js +2 -10
  36. package/src/isv/poi/set-price.js +217 -225
  37. package/src/isv/products/audios.js +0 -0
  38. package/src/isv/products/create-product.js +313 -313
  39. package/src/isv/products/delete-product.js +0 -0
  40. package/src/isv/products/get-product-image.js +0 -0
  41. package/src/isv/products/get-product.js +0 -0
  42. package/src/isv/products/index.js +0 -0
  43. package/src/isv/products/list-products.js +0 -0
  44. package/src/isv/products/schedules.js +0 -0
  45. package/src/isv/products/update-product.js +264 -264
  46. package/src/isv/reference/index.js +249 -0
  47. package/src/isv/shops/index.js +22 -0
  48. package/src/isv/transactions/cancel-transaction.js +43 -12
  49. package/src/isv/transactions/create-transaction.js +22 -6
  50. package/src/isv/transactions/get-status.js +18 -11
  51. package/src/isv/transactions/process-transaction.js +160 -42
  52. package/src/isv/transactions/simulate-callback.js +128 -109
  53. package/src/isv/transactions/transaction-history.js +86 -90
  54. package/src/isv/utils/auth.js +2 -2
  55. package/src/mcp-proxy.js +0 -0
  56. package/src/merchant/data/export-report.js +0 -0
  57. package/src/merchant/data/generate-report.js +19 -1
  58. package/src/merchant/data/get-export-list.js +0 -0
  59. package/src/merchant/data/get-report-requests.js +0 -0
  60. package/src/merchant/data/get-report-status.js +0 -0
  61. package/src/merchant/data/index.js +0 -0
  62. package/src/merchant/data/list-reports.js +0 -0
  63. package/src/merchant/deep-links/delete-product-link.js +0 -0
  64. package/src/merchant/deep-links/delete-transaction-link.js +0 -0
  65. package/src/merchant/deep-links/get-product-link.js +0 -0
  66. package/src/merchant/deep-links/get-transaction-link.js +0 -0
  67. package/src/merchant/deep-links/index.js +0 -0
  68. package/src/merchant/index.js +21 -1
  69. package/src/merchant/logs/index.js +366 -0
  70. package/src/merchant/products/audios.js +0 -0
  71. package/src/merchant/products/create-product.js +287 -287
  72. package/src/merchant/products/delete-product.js +0 -0
  73. package/src/merchant/products/get-product-image.js +0 -0
  74. package/src/merchant/products/get-product.js +0 -0
  75. package/src/merchant/products/index.js +0 -0
  76. package/src/merchant/products/list-products.js +0 -0
  77. package/src/merchant/products/schedules.js +0 -0
  78. package/src/merchant/products/update-product.js +281 -281
  79. package/src/merchant/shops/index.js +18 -0
  80. package/src/merchant/transactions/cancel-transaction.js +16 -19
  81. package/src/merchant/transactions/create-transaction.js +99 -19
  82. package/src/merchant/transactions/get-status.js +41 -12
  83. package/src/merchant/transactions/process-transaction.js +8 -16
  84. package/src/merchant/transactions/simulate-callback.js +68 -9
  85. package/src/merchant/transactions/transaction-history.js +275 -257
  86. package/src/payment-institution/data/index.js +0 -0
  87. package/src/payment-institution/deep-links/delete-transaction-link.js +0 -0
  88. package/src/payment-institution/deep-links/get-transaction-link.js +0 -0
  89. package/src/payment-institution/deep-links/index.js +0 -0
  90. package/src/payment-institution/index.js +0 -0
  91. package/src/payment-institution/transactions/create-pi-transaction.js +6 -14
  92. package/src/payment-institution/transactions/finalize-transaction.js +333 -340
  93. package/src/payment-institution/transactions/get-transaction-history.js +3 -11
  94. package/src/payment-institution/transactions/get-transaction-status.js +39 -12
  95. package/src/payment-institution/transactions/process-pi-transaction.js +5 -13
  96. package/src/payment-institution/transactions/simulate-pi-callback.js +49 -9
  97. package/src/payment-institution/transactions/soundbite-transaction.js +5 -13
  98. package/src/proxy-server.js +0 -0
  99. package/src/shared/api-errors.js +188 -0
  100. package/src/shared/auth-headers.js +62 -0
  101. package/src/shared/data/data-api.js +11 -10
  102. package/src/shared/deep-links/api.js +5 -4
  103. package/src/shared/index.js +0 -0
  104. package/src/shared/logs/logs-api.js +242 -0
  105. package/src/shared/poi/poi-create-api.js +419 -0
  106. package/src/shared/products/products-api.js +17 -16
  107. package/src/shared/shops/shops-api.js +155 -0
  108. package/src/tools/advanced-code-generator.js +2255 -2246
  109. package/src/utils/code-examples/common/auth-examples.js +1484 -1481
  110. package/src/utils/code-examples/common/helpers.js +7 -4
  111. package/src/utils/code-examples/common/utils-examples.js +0 -0
  112. package/src/utils/code-examples/frameworks/examples.js +4 -4
  113. package/src/utils/code-examples/index.js +0 -0
  114. package/src/utils/code-examples/isv/oauth2.js +458 -600
  115. package/src/utils/code-examples/isv/products.js +8 -8
  116. package/src/utils/code-examples/isv/transactions.js +815 -812
  117. package/src/utils/code-examples/merchant/products.js +0 -0
  118. package/src/utils/code-examples/merchant/transactions.js +9 -9
  119. package/src/utils/code-examples/payment-institution/transactions.js +0 -0
  120. package/src/utils/code-examples/shared/data.js +0 -0
  121. package/src/utils/code-examples/shared/deep-links.js +0 -0
  122. package/src/utils/code-examples/shared/soundbites.js +0 -0
  123. package/src/utils/code-examples.js +24 -24
  124. package/src/utils/format-request.js +3 -3
  125. package/src/utils/json-formatter.js +20 -20
  126. package/src/utils/server-info.js +0 -0
  127. package/MCP-SETUP.md +0 -190
@@ -1,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 contentMd5 needed for GET)
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 contentMd5 needed for GET)
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 payment institution.
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 MD5 calculation
77
+ // Convert to deterministic minimized JSON for SHA-256 calculation
77
78
  const minimizedBodyString = createMinimizedJSON(requestBody);
78
79
 
79
- // Create JWT token with contentMd5 for the request body (POST requires contentMd5)
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 MD5 calculation
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 Java implementation: |payerAmount - amount| or |payeeAmount - amount|
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
- fee,
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
- fee,
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
- finalized: Date.now()
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 = '0.60',
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 contentMd5 the same way
41
- // The JWT token is created without content MD5 for multipart uploads
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 contentMd5 (multipart upload)`,
93
+ - JWT authentication without contentSha256 (multipart upload)`,
102
94
  inputSchema: {
103
95
  type: "object",
104
96
  properties: {
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
+ }