@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
@@ -0,0 +1,249 @@
1
+ import axios from 'axios';
2
+ import { createJWTToken } from '../../core/auth/jwt-token.js';
3
+ import { apiErrorResult } from '../../shared/api-errors.js';
4
+ import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
5
+
6
+ /**
7
+ * Reference data - the enumerations a merchant registration form has to be filled in with.
8
+ *
9
+ * These are the values an assistant otherwise guesses at, and guessing is expensive here: a legal
10
+ * form is jurisdiction-specific ("OOD" exists in Bulgaria and not in Spain), a business activity is
11
+ * a numeric id rather than free text, and a currency the platform does not carry is a rejected
12
+ * transaction rather than a rejected form. One GET each and there is nothing to guess.
13
+ *
14
+ * **Authenticated as the ISV itself, not on behalf of a merchant.** These are consumed while
15
+ * building the form that creates a merchant, so there is no merchant to act for yet - the request
16
+ * carries a plain ISV JWT with no `aud`/`sub` on-behalf claims.
17
+ */
18
+
19
+ /** The five datasets, and what each one's query parameters mean. */
20
+ const DATASETS = {
21
+ 'legal-forms': {
22
+ path: '/isv/reference/legal-forms',
23
+ requires: 'jurisdictionCode',
24
+ summary: 'Legal forms (company types) available in one jurisdiction',
25
+ note:
26
+ 'jurisdictionCode is REQUIRED - an ISO 3166-1 alpha-2 country code such as BG, DE, ES. ' +
27
+ 'An unknown code is not an error: the server falls back to a generic jurisdiction and returns ' +
28
+ 'its list, so a typo comes back looking like a valid but oddly generic answer. Check that the ' +
29
+ 'result matches the country you meant.'
30
+ },
31
+ 'business-activities': {
32
+ path: '/isv/reference/business-activities',
33
+ optional: 'languageCode',
34
+ summary: 'Business activity categories, translated',
35
+ note:
36
+ 'languageCode is an ISO 639-1 code (en, bg, es) and defaults to English. Only the LABELS are ' +
37
+ 'translated - the ids are stable across languages, so store the id and re-fetch labels rather ' +
38
+ 'than storing the text.'
39
+ },
40
+ currencies: {
41
+ path: '/isv/reference/currencies',
42
+ summary: 'Every currency payware supports',
43
+ note:
44
+ 'Use this to validate a currency before sending it on a transaction or product. Note that the ' +
45
+ 'number of decimal places a currency uses is a property OF THE CURRENCY - two for EUR, none ' +
46
+ 'for JPY, more for others - and every amount on the API is formatted to it. Do not assume two.'
47
+ },
48
+ languages: {
49
+ path: '/isv/reference/languages',
50
+ summary: 'Languages payware supports for merchant-facing content',
51
+ note: 'The set that languageCode accepts on the other datasets, and that a merchant can be registered in.'
52
+ },
53
+ jurisdictions: {
54
+ path: '/isv/reference/jurisdictions',
55
+ optional: 'languageCode',
56
+ summary: 'Countries/jurisdictions where payware operates, translated',
57
+ note:
58
+ 'Fetch this before legal-forms: it gives you the valid jurisdictionCode values, which is the ' +
59
+ 'one parameter legal-forms will silently fall back on rather than reject.'
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Fetch one reference dataset.
65
+ *
66
+ * @param {Object} params
67
+ * @param {string} params.dataset one of the keys of DATASETS
68
+ * @param {string} [params.jurisdictionCode] required for legal-forms
69
+ * @param {string} [params.languageCode] optional for business-activities and jurisdictions
70
+ */
71
+ export async function getReferenceData({
72
+ dataset,
73
+ jurisdictionCode,
74
+ languageCode,
75
+ partnerId,
76
+ privateKey,
77
+ useSandbox = true
78
+ }) {
79
+ const spec = DATASETS[dataset];
80
+ if (!spec) {
81
+ throw new Error(
82
+ `Unknown dataset "${dataset}". Valid datasets: ${Object.keys(DATASETS).join(', ')}`
83
+ );
84
+ }
85
+ if (spec.requires === 'jurisdictionCode' && !jurisdictionCode) {
86
+ throw new Error(
87
+ 'jurisdictionCode is required for the legal-forms dataset. Use the jurisdictions dataset to ' +
88
+ 'find valid codes - an unknown code is NOT rejected, it silently falls back to a generic list.'
89
+ );
90
+ }
91
+
92
+ const query = new URLSearchParams();
93
+ if (spec.requires === 'jurisdictionCode' || jurisdictionCode) {
94
+ if (jurisdictionCode) query.set('jurisdictionCode', jurisdictionCode.toUpperCase());
95
+ }
96
+ if (spec.optional === 'languageCode' && languageCode) {
97
+ query.set('languageCode', languageCode);
98
+ }
99
+
100
+ // GET, so no body and no content hash.
101
+ const tokenData = createJWTToken(partnerId, privateKey);
102
+ const headers = {
103
+ 'Authorization': `Bearer ${tokenData.token}`,
104
+ 'Content-Type': 'application/json',
105
+ 'Api-Version': '1'
106
+ };
107
+
108
+ try {
109
+ const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
110
+ const qs = query.toString();
111
+ const response = await axios.get(`${baseUrl}${spec.path}${qs ? `?${qs}` : ''}`, { headers });
112
+
113
+ return {
114
+ success: true,
115
+ dataset,
116
+ items: response.data,
117
+ count: Array.isArray(response.data) ? response.data.length : 0,
118
+ requestId: response.headers['x-request-id'],
119
+ timestamp: new Date().toISOString()
120
+ };
121
+ } catch (error) {
122
+ return apiErrorResult(error);
123
+ }
124
+ }
125
+
126
+ const DATASET_DOCS = Object.entries(DATASETS)
127
+ .map(([key, spec]) => `- **\`${key}\`** - ${spec.summary}.\n ${spec.note}`)
128
+ .join('\n');
129
+
130
+ export const referenceDataTool = {
131
+ name: 'payware_reference_lookup',
132
+ description: `Look up payware reference data - the enumerations a merchant registration form needs.
133
+
134
+ **Endpoints:** GET /isv/reference/{legal-forms, business-activities, currencies, languages, jurisdictions}
135
+
136
+ One tool with a \`dataset\` parameter rather than five near-identical tools. All five are cheap,
137
+ read-only lookups, and they are used together when building a registration flow.
138
+
139
+ **Datasets**
140
+
141
+ ${DATASET_DOCS}
142
+
143
+ **Authentication:** as the ISV itself. These are consumed while building the form that CREATES a
144
+ merchant, so there is no merchant to act on behalf of yet - no \`merchantPartnerId\` or
145
+ \`oauth2Token\` is involved.
146
+
147
+ **Typical order when registering a merchant:** \`jurisdictions\` -> \`legal-forms\` (with the chosen
148
+ jurisdictionCode) -> \`business-activities\` -> \`currencies\`. Store the **ids** these return, not the
149
+ translated labels; the labels change with \`languageCode\` and the ids do not.`,
150
+
151
+ inputSchema: {
152
+ type: 'object',
153
+ required: ['dataset'],
154
+ properties: {
155
+ dataset: {
156
+ type: 'string',
157
+ enum: Object.keys(DATASETS),
158
+ description: 'Which reference dataset to fetch.'
159
+ },
160
+ jurisdictionCode: {
161
+ type: 'string',
162
+ description: "ISO 3166-1 alpha-2 country code (BG, DE, ES). REQUIRED for dataset='legal-forms'. An unknown code is not rejected - the server falls back to a generic jurisdiction - so verify the result matches the country you meant.",
163
+ pattern: '^[A-Za-z]{2}$'
164
+ },
165
+ languageCode: {
166
+ type: 'string',
167
+ description: "ISO 639-1 language code (en, bg, es) for translated labels. Applies to dataset='business-activities' and dataset='jurisdictions'; ignored otherwise. Defaults to English.",
168
+ pattern: '^[A-Za-z]{2}$'
169
+ },
170
+ partnerId: {
171
+ type: 'string',
172
+ description: 'ISV partner ID. Uses PAYWARE_PARTNER_ID env var as default.'
173
+ },
174
+ privateKey: {
175
+ type: 'string',
176
+ description: 'RSA private key for JWT signing. Uses the environment-specific key as default.'
177
+ },
178
+ useSandbox: {
179
+ type: 'boolean',
180
+ description: 'Use sandbox environment for testing',
181
+ default: true
182
+ }
183
+ }
184
+ },
185
+
186
+ async handler(args) {
187
+ const {
188
+ dataset,
189
+ jurisdictionCode,
190
+ languageCode,
191
+ partnerId = getPartnerIdSafe(),
192
+ privateKey = getPrivateKeySafe(args.useSandbox ?? true),
193
+ useSandbox = true
194
+ } = args;
195
+
196
+ if (!partnerId) {
197
+ throw new Error("Partner ID is required. Provide 'partnerId' or set PAYWARE_PARTNER_ID.");
198
+ }
199
+ if (!privateKey) {
200
+ throw new Error("Private key is required. Provide 'privateKey' or set PAYWARE_PRIVATE_KEY.");
201
+ }
202
+
203
+ const result = await getReferenceData({
204
+ dataset, jurisdictionCode, languageCode, partnerId, privateKey, useSandbox
205
+ });
206
+
207
+ if (!result.success) {
208
+ return {
209
+ content: [{
210
+ type: 'text',
211
+ text: `❌ **Reference Lookup Failed** (${dataset})
212
+
213
+ - Message: ${result.error.message}
214
+ - Status: ${result.error.status || 'N/A'}
215
+ - Code: ${result.error.errorCode || 'N/A'}
216
+ ${result.error.correlationId ? `- Correlation ID: ${result.error.correlationId}` : ''}
217
+ ${result.error.guidance ? `\n${result.error.guidance}` : ''}
218
+
219
+ **Timestamp:** ${result.timestamp}`
220
+ }]
221
+ };
222
+ }
223
+
224
+ const items = Array.isArray(result.items) ? result.items : [];
225
+ // Reference rows have no single shape, so render whatever identity-ish and label-ish fields
226
+ // each one actually has rather than assuming a schema that differs per dataset.
227
+ const rendered = items.map(item => {
228
+ const id = item.id ?? item.code ?? item.isoCode ?? item.currencyCode ?? item.iso31661Alpha2;
229
+ const label = item.name ?? item.description ?? item.label ?? item.translation;
230
+ return `- ${id !== undefined ? `\`${id}\`` : ''}${label ? ` ${label}` : ''}`.trim();
231
+ }).join('\n');
232
+
233
+ return {
234
+ content: [{
235
+ type: 'text',
236
+ text: `📚 **${dataset}** - ${result.count} item(s)${jurisdictionCode ? ` for ${jurisdictionCode.toUpperCase()}` : ''}${languageCode ? ` in ${languageCode}` : ''}
237
+
238
+ ${rendered || 'No items returned.'}
239
+
240
+ ${DATASETS[dataset].note}
241
+
242
+ **Request ID:** ${result.requestId || 'N/A'}
243
+ **Timestamp:** ${result.timestamp}`
244
+ }]
245
+ };
246
+ }
247
+ };
248
+
249
+ export const isvReferenceTools = [referenceDataTool];
@@ -0,0 +1,22 @@
1
+ import { buildListShopsTool } from '../../shared/shops/shops-api.js';
2
+ import { getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
3
+
4
+ /**
5
+ * Shop tools for the ISV role.
6
+ *
7
+ * Signed as an on-behalf call, so the server answers with the shops this merchant assigned to this
8
+ * ISV's scope - not every shop the merchant owns. Anything outside that scope is a 403
9
+ * ERR_SHOP_NOT_IN_SCOPE on the write paths, which is why listing matters more here than it does for
10
+ * a merchant acting on its own estate.
11
+ */
12
+ export const listShopsTool = buildListShopsTool({
13
+ partnerType: 'isv',
14
+ resolveAuth: (args) => ({
15
+ isvPartnerId: getPartnerIdSafe(),
16
+ privateKey: getPrivateKeySafe(args.useSandbox ?? true),
17
+ merchantPartnerId: args.merchantPartnerId,
18
+ oauth2Token: args.oauth2Token
19
+ })
20
+ });
21
+
22
+ export const isvShopTools = [listShopsTool];
@@ -1,15 +1,26 @@
1
1
  import axios from 'axios';
2
2
  import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
3
3
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
4
+ import { describeApiError } from '../../shared/api-errors.js';
4
5
 
5
6
  /**
6
7
  * Cancel a transaction via payware API as an ISV
7
8
  */
8
- export async function cancelTransaction({ transactionId, merchantPartnerId, oauth2Token, useSandbox = true }) {
9
+ export async function cancelTransaction({ transactionId, statusMessage, merchantPartnerId, oauth2Token, useSandbox = true }) {
9
10
  if (!transactionId) {
10
11
  throw new Error('Transaction ID is required');
11
12
  }
12
13
 
14
+ // Required by the server, not optional politeness: MerchantValidationService throws
15
+ // MissingStatusMessageException when an ACTIVE transaction is cancelled without one, and that
16
+ // path is the ISV path too - it explicitly resolves the acting merchant through the on-behalf ISV.
17
+ if (!statusMessage) {
18
+ throw new Error('statusMessage is required when cancelling (server: ERR_MISSING_STATUS_MESSAGE)');
19
+ }
20
+ if (statusMessage.length > 100) {
21
+ throw new Error('statusMessage cannot exceed 100 characters');
22
+ }
23
+
13
24
  if (!merchantPartnerId) {
14
25
  throw new Error('Merchant Partner ID is required for ISV operations');
15
26
  }
@@ -21,7 +32,17 @@ export async function cancelTransaction({ transactionId, merchantPartnerId, oaut
21
32
  const isvPartnerId = getPartnerIdSafe();
22
33
  const privateKey = getPrivateKeySafe(useSandbox);
23
34
 
24
- const requestBody = {}; // Empty body for cancel
35
+ // PATCH with a status body, matching the merchant tool and the server.
36
+ //
37
+ // This used to be `axios.delete` against `/transactions/{id}` with an empty body. There is no
38
+ // DELETE /transactions/{id} on the server - TransactionController has DELETE only on
39
+ // /{transactionId}/link - so the call could never have succeeded, and the empty body carried
40
+ // neither the CANCELLED status nor the status message the validator demands. Three separate
41
+ // reasons it could not work, which is why it is a rewrite rather than a patch.
42
+ const requestBody = {
43
+ status: 'CANCELLED',
44
+ statusMessage
45
+ };
25
46
 
26
47
  const jwtData = await createJWTForPartner({
27
48
  partnerId: isvPartnerId,
@@ -31,30 +52,26 @@ export async function cancelTransaction({ transactionId, merchantPartnerId, oaut
31
52
  oauth2Token
32
53
  });
33
54
 
34
- // Use the same JSON serialization method as the JWT factory for MD5 calculation
55
+ // Use the same JSON serialization method as the JWT factory for SHA-256 calculation
35
56
  const { createMinimizedJSON } = await import('../../core/utils/json-serializer.js');
36
57
  const minimizedBodyString = createMinimizedJSON(requestBody);
37
58
 
38
59
  try {
39
60
  const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
40
- // Send the exact minimized JSON string that was used for MD5 calculation
41
- const response = await axios.delete(`${baseUrl}/transactions/${transactionId}`, {
61
+ // Send the exact minimized JSON string that was used for SHA-256 calculation
62
+ const response = await axios.patch(`${baseUrl}/transactions/${transactionId}`, minimizedBodyString, {
42
63
  headers: {
43
64
  'Authorization': `Bearer ${jwtData.token}`,
44
65
  'Content-Type': 'application/json',
45
66
  'Api-Version': '1'
46
67
  },
47
- data: minimizedBodyString,
48
68
  // Tell axios to send the string as-is, don't serialize it again
49
69
  transformRequest: [(data) => data]
50
70
  });
51
71
 
52
72
  return response.data;
53
73
  } catch (error) {
54
- if (error.response) {
55
- throw new Error(`payware API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
56
- }
57
- throw error;
74
+ throw describeApiError(error, 'call the payware API');
58
75
  }
59
76
  }
60
77
 
@@ -62,17 +79,31 @@ export const cancelTransactionTool = {
62
79
  name: 'payware_operations_cancel_transaction',
63
80
  description: `Cancel an active transaction as an ISV on behalf of a merchant.
64
81
 
82
+ **Endpoint:** PATCH /transactions/{transactionId} with \`{"status":"CANCELLED","statusMessage":"..."}\`
83
+
65
84
  Requires ISV authentication with merchant partner ID and OAuth2 token.
66
- Only ACTIVE transactions can be cancelled.`,
85
+ Only ACTIVE transactions can be cancelled - one already processed answers 409 ERR_ALREADY_PROCESSED.
86
+
87
+ **\`statusMessage\` is mandatory.** The server rejects a cancellation without one
88
+ (ERR_MISSING_STATUS_MESSAGE); it is the reason shown to the merchant and recorded against the
89
+ transaction. Max 100 characters.
90
+
91
+ **You can cancel only what you initiated.** An ISV's own transactions include those it created on
92
+ behalf of this merchant; anything else answers 403 ERR_UNAUTHORIZED_OPERATION.`,
67
93
 
68
94
  inputSchema: {
69
95
  type: 'object',
70
- required: ['transactionId', 'merchantPartnerId', 'oauth2Token'],
96
+ required: ['transactionId', 'statusMessage', 'merchantPartnerId', 'oauth2Token'],
71
97
  properties: {
72
98
  transactionId: {
73
99
  type: 'string',
74
100
  description: 'Transaction ID to cancel'
75
101
  },
102
+ statusMessage: {
103
+ type: 'string',
104
+ description: 'Reason for the cancellation. REQUIRED - the server rejects a cancellation without one (ERR_MISSING_STATUS_MESSAGE). Max 100 characters.',
105
+ maxLength: 100
106
+ },
76
107
  merchantPartnerId: {
77
108
  type: 'string',
78
109
  description: 'Partner ID of the target merchant'
@@ -1,6 +1,7 @@
1
1
  import axios from 'axios';
2
2
  import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
3
3
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
4
+ import { describeApiError } from '../../shared/api-errors.js';
4
5
 
5
6
  /**
6
7
  * Create a transaction via payware API on behalf of a merchant (ISV)
@@ -119,13 +120,13 @@ export async function createTransaction({
119
120
  oauth2Token
120
121
  });
121
122
 
122
- // Use the same JSON serialization method as the JWT factory for MD5 calculation
123
+ // Use the same JSON serialization method as the JWT factory for SHA-256 calculation
123
124
  const { createMinimizedJSON } = await import('../../core/utils/json-serializer.js');
124
125
  const minimizedBodyString = createMinimizedJSON(requestBody);
125
126
 
126
127
  try {
127
128
  const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
128
- // Send the exact minimized JSON string that was used for MD5 calculation
129
+ // Send the exact minimized JSON string that was used for SHA-256 calculation
129
130
  const response = await axios.post(`${baseUrl}/transactions`, minimizedBodyString, {
130
131
  headers: {
131
132
  'Authorization': `Bearer ${jwtData.token}`,
@@ -138,10 +139,7 @@ export async function createTransaction({
138
139
 
139
140
  return response.data;
140
141
  } catch (error) {
141
- if (error.response) {
142
- throw new Error(`payware API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
143
- }
144
- throw error;
142
+ throw describeApiError(error, 'call the payware API');
145
143
  }
146
144
  }
147
145
 
@@ -154,6 +152,24 @@ Requires valid OAuth2 token obtained from the merchant.
154
152
 
155
153
  **Authentication**: Uses ISV JWT with merchant partner ID in 'aud' claim and OAuth2 token in 'sub' claim.
156
154
 
155
+ ⚠️ **The MERCHANT's plan governs, not the ISV's.** An on-behalf call inherits the plan of the merchant
156
+ it acts for, so this endpoint answers **403** (\`ERR_METHOD_NOT_ALLOWED_FOR_CURRENT_USER_PLAN\`) when
157
+ that merchant is on **Basic**. Acting on behalf is no longer sufficient on its own - before
158
+ 2026-08-21 it was, and an ISV serving a Basic merchant could reach the transaction, product, shop and
159
+ report APIs regardless of that merchant's plan. The same applies to
160
+ \`payware_products_*\`, \`payware_deep_links_*\` and \`payware_data_*\`. If a call that used to work
161
+ now returns 403, check the merchant's plan first.
162
+
163
+ ⚠️ **Shop scope.** An ISV reaches only the shops the merchant assigned it. Naming any other shop
164
+ answers **403 ERR_SHOP_NOT_IN_SCOPE**, and \`payware_shops_list\` returns only the in-scope ones.
165
+ Omitting \`shop\` uses the merchant's default shop, which may itself be out of scope.
166
+
167
+ ℹ️ **Producer attribution does not apply here.** \`producerPartnerId\` / \`terminalId\` /
168
+ \`terminalManufacturer\` are accepted on direct merchant calls only. Whoever made the API call owns the
169
+ sale, so an ISV call is an ISV sale even when the ISV's software is running on a hardware producer's
170
+ terminal. Do not try to pass them through - use \`payware_operations_create_transaction\` from the
171
+ merchant role if the terminal producer is meant to be attributed.
172
+
157
173
  **Transaction Types:**
158
174
  - PLAIN: Basic transaction without visual codes
159
175
  - QR: Transaction with QR code for mobile payments
@@ -1,6 +1,7 @@
1
1
  import axios from 'axios';
2
2
  import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
3
3
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
4
+ import { apiErrorResult } from '../../shared/api-errors.js';
4
5
 
5
6
  /**
6
7
  * Get transaction status from payware API as an ISV
@@ -51,16 +52,7 @@ export async function getTransactionStatus({ transactionId, merchantPartnerId, o
51
52
  timestamp: new Date().toISOString()
52
53
  };
53
54
  } catch (error) {
54
- return {
55
- success: false,
56
- error: {
57
- message: error.response?.data?.message || error.message,
58
- status: error.response?.status,
59
- code: error.response?.data?.code,
60
- details: error.response?.data
61
- },
62
- timestamp: new Date().toISOString()
63
- };
55
+ return apiErrorResult(error);
64
56
  }
65
57
  }
66
58
 
@@ -96,7 +88,22 @@ export const getTransactionStatusTool = {
96
88
  **Use Case:** Check status of ACTIVE transactions only. This endpoint only returns transactions with ACTIVE status.
97
89
  **Note:** For transactions with final statuses (CONFIRMED, DECLINED, FAILED, EXPIRED, CANCELLED), use 'payware_transactions_get_history' instead.
98
90
 
99
- **Required:** Merchant Partner ID and OAuth2 token for ISV authentication.`,
91
+ **Required:** Merchant Partner ID and OAuth2 token for ISV authentication.
92
+
93
+ 🛍️ **Product-originated transactions carry a nested \`product\` object** (productId, type, name,
94
+ descriptions, imageUrl, imageDigest, imageContentType, imageBytes, termsUrl, termsText, regularPrice,
95
+ shippable, quickPay, shopName). Since 2026-07-22 these are grouped under \`product\` instead of sitting
96
+ flat - the old top-level \`imageUrl\` is gone. Discriminate by the presence of the object. Verify
97
+ \`imageDigest\` (SHA-256, pinned by payware) before rendering the image to a payer; \`regularPrice\` is
98
+ display-only and appears only when higher than \`amount\`. The top-level \`amount\` and \`currency\` are
99
+ authoritative and are not repeated inside the object.
100
+
101
+ ⚠️ **The MERCHANT's plan governs.** An on-behalf call inherits the acting merchant's plan, so this
102
+ answers **403** for a **Basic** merchant. Which presentation fields come back also follows that plan:
103
+ \`imageUrl\` and \`termsUrl\` are Standard and above.
104
+
105
+ 💰 **Amounts follow the currency, not a fixed two decimals.** Parse them with a decimal type and never
106
+ compare them as strings.`,
100
107
 
101
108
  inputSchema: {
102
109
  type: "object",