@payware/mcp-server 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -0
- package/README.md +22 -16
- package/check-keys.js +0 -0
- package/keys/README.md +0 -0
- package/mcp-config.json +0 -0
- package/package.json +69 -66
- package/payware-mcp-config.json +0 -0
- package/src/auth/rsa-keys.js +0 -0
- package/src/auth/sandbox-setup.js +186 -186
- package/src/config/env.js +0 -0
- package/src/config/partner-types.js +0 -0
- package/src/core/auth/jwt-factory.js +4 -4
- package/src/core/auth/jwt-token.js +107 -66
- package/src/core/utils/json-serializer.js +5 -5
- package/src/core/utils/key-utils.js +0 -0
- package/src/index.js +0 -0
- package/src/isv/data/index.js +0 -0
- package/src/isv/deep-links/delete-product-link.js +0 -0
- package/src/isv/deep-links/delete-transaction-link.js +0 -0
- package/src/isv/deep-links/get-product-link.js +0 -0
- package/src/isv/deep-links/get-transaction-link.js +0 -0
- package/src/isv/deep-links/index.js +0 -0
- package/src/isv/index.js +29 -1
- package/src/isv/invitations/index.js +471 -0
- package/src/isv/oauth2/create-token-simple.js +0 -0
- package/src/isv/oauth2/get-token-info.js +0 -0
- package/src/isv/oauth2/obtain-token.js +19 -8
- package/src/isv/oauth2/token-helper.js +5 -2
- package/src/isv/poi/cancel-price.js +2 -10
- package/src/isv/poi/create.js +19 -0
- package/src/isv/poi/get-poi.js +2 -10
- package/src/isv/poi/get-qrcode.js +2 -10
- package/src/isv/poi/get-status.js +2 -10
- package/src/isv/poi/index.js +5 -0
- package/src/isv/poi/list-pois.js +2 -10
- package/src/isv/poi/set-price.js +217 -225
- package/src/isv/products/audios.js +0 -0
- package/src/isv/products/create-product.js +313 -313
- package/src/isv/products/delete-product.js +0 -0
- package/src/isv/products/get-product-image.js +0 -0
- package/src/isv/products/get-product.js +0 -0
- package/src/isv/products/index.js +0 -0
- package/src/isv/products/list-products.js +0 -0
- package/src/isv/products/schedules.js +0 -0
- package/src/isv/products/update-product.js +264 -264
- package/src/isv/reference/index.js +249 -0
- package/src/isv/shops/index.js +22 -0
- package/src/isv/transactions/cancel-transaction.js +43 -12
- package/src/isv/transactions/create-transaction.js +22 -6
- package/src/isv/transactions/get-status.js +18 -11
- package/src/isv/transactions/process-transaction.js +160 -42
- package/src/isv/transactions/simulate-callback.js +128 -109
- package/src/isv/transactions/transaction-history.js +86 -90
- package/src/isv/utils/auth.js +2 -2
- package/src/mcp-proxy.js +0 -0
- package/src/merchant/data/export-report.js +0 -0
- package/src/merchant/data/generate-report.js +19 -1
- package/src/merchant/data/get-export-list.js +0 -0
- package/src/merchant/data/get-report-requests.js +0 -0
- package/src/merchant/data/get-report-status.js +0 -0
- package/src/merchant/data/index.js +0 -0
- package/src/merchant/data/list-reports.js +0 -0
- package/src/merchant/deep-links/delete-product-link.js +0 -0
- package/src/merchant/deep-links/delete-transaction-link.js +0 -0
- package/src/merchant/deep-links/get-product-link.js +0 -0
- package/src/merchant/deep-links/get-transaction-link.js +0 -0
- package/src/merchant/deep-links/index.js +0 -0
- package/src/merchant/index.js +21 -1
- package/src/merchant/logs/index.js +366 -0
- package/src/merchant/products/audios.js +0 -0
- package/src/merchant/products/create-product.js +287 -287
- package/src/merchant/products/delete-product.js +0 -0
- package/src/merchant/products/get-product-image.js +0 -0
- package/src/merchant/products/get-product.js +0 -0
- package/src/merchant/products/index.js +0 -0
- package/src/merchant/products/list-products.js +0 -0
- package/src/merchant/products/schedules.js +0 -0
- package/src/merchant/products/update-product.js +281 -281
- package/src/merchant/shops/index.js +18 -0
- package/src/merchant/transactions/cancel-transaction.js +16 -19
- package/src/merchant/transactions/create-transaction.js +99 -19
- package/src/merchant/transactions/get-status.js +41 -12
- package/src/merchant/transactions/process-transaction.js +8 -16
- package/src/merchant/transactions/simulate-callback.js +68 -9
- package/src/merchant/transactions/transaction-history.js +275 -257
- package/src/payment-institution/data/index.js +0 -0
- package/src/payment-institution/deep-links/delete-transaction-link.js +0 -0
- package/src/payment-institution/deep-links/get-transaction-link.js +0 -0
- package/src/payment-institution/deep-links/index.js +0 -0
- package/src/payment-institution/index.js +0 -0
- package/src/payment-institution/transactions/create-pi-transaction.js +6 -14
- package/src/payment-institution/transactions/finalize-transaction.js +333 -340
- package/src/payment-institution/transactions/get-transaction-history.js +3 -11
- package/src/payment-institution/transactions/get-transaction-status.js +39 -12
- package/src/payment-institution/transactions/process-pi-transaction.js +5 -13
- package/src/payment-institution/transactions/simulate-pi-callback.js +49 -9
- package/src/payment-institution/transactions/soundbite-transaction.js +5 -13
- package/src/proxy-server.js +0 -0
- package/src/shared/api-errors.js +188 -0
- package/src/shared/auth-headers.js +62 -0
- package/src/shared/data/data-api.js +11 -10
- package/src/shared/deep-links/api.js +5 -4
- package/src/shared/index.js +0 -0
- package/src/shared/logs/logs-api.js +242 -0
- package/src/shared/poi/poi-create-api.js +419 -0
- package/src/shared/products/products-api.js +17 -16
- package/src/shared/shops/shops-api.js +155 -0
- package/src/tools/advanced-code-generator.js +2255 -2246
- package/src/utils/code-examples/common/auth-examples.js +1484 -1481
- package/src/utils/code-examples/common/helpers.js +7 -4
- package/src/utils/code-examples/common/utils-examples.js +0 -0
- package/src/utils/code-examples/frameworks/examples.js +4 -4
- package/src/utils/code-examples/index.js +0 -0
- package/src/utils/code-examples/isv/oauth2.js +458 -600
- package/src/utils/code-examples/isv/products.js +8 -8
- package/src/utils/code-examples/isv/transactions.js +815 -812
- package/src/utils/code-examples/merchant/products.js +0 -0
- package/src/utils/code-examples/merchant/transactions.js +9 -9
- package/src/utils/code-examples/payment-institution/transactions.js +0 -0
- package/src/utils/code-examples/shared/data.js +0 -0
- package/src/utils/code-examples/shared/deep-links.js +0 -0
- package/src/utils/code-examples/shared/soundbites.js +0 -0
- package/src/utils/code-examples.js +24 -24
- package/src/utils/format-request.js +3 -3
- package/src/utils/json-formatter.js +20 -20
- package/src/utils/server-info.js +0 -0
- package/MCP-SETUP.md +0 -190
|
@@ -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
|
-
|
|
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
|
|
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
|
|
41
|
-
const response = await axios.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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",
|