@payware/mcp-server 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/package.json +69 -66
- package/src/auth/sandbox-setup.js +186 -186
- package/src/isv/index.js +29 -1
- package/src/isv/invitations/index.js +471 -0
- package/src/isv/oauth2/obtain-token.js +19 -8
- package/src/isv/oauth2/token-helper.js +5 -2
- package/src/isv/poi/cancel-price.js +2 -10
- package/src/isv/poi/create.js +19 -0
- package/src/isv/poi/get-poi.js +2 -10
- package/src/isv/poi/get-qrcode.js +2 -10
- package/src/isv/poi/get-status.js +2 -10
- package/src/isv/poi/index.js +5 -0
- package/src/isv/poi/list-pois.js +2 -10
- package/src/isv/poi/set-price.js +217 -225
- package/src/isv/products/create-product.js +313 -313
- package/src/isv/products/update-product.js +264 -264
- package/src/isv/reference/index.js +249 -0
- package/src/isv/shops/index.js +22 -0
- package/src/isv/transactions/cancel-transaction.js +41 -10
- package/src/isv/transactions/create-transaction.js +20 -4
- package/src/isv/transactions/get-status.js +18 -11
- package/src/isv/transactions/process-transaction.js +160 -42
- package/src/isv/transactions/simulate-callback.js +128 -109
- package/src/isv/transactions/transaction-history.js +86 -90
- package/src/merchant/data/generate-report.js +19 -1
- package/src/merchant/index.js +21 -1
- package/src/merchant/logs/index.js +366 -0
- package/src/merchant/products/create-product.js +287 -287
- package/src/merchant/products/update-product.js +281 -281
- package/src/merchant/shops/index.js +18 -0
- package/src/merchant/transactions/cancel-transaction.js +10 -13
- package/src/merchant/transactions/create-transaction.js +88 -8
- package/src/merchant/transactions/get-status.js +40 -11
- package/src/merchant/transactions/process-transaction.js +2 -10
- package/src/merchant/transactions/simulate-callback.js +68 -9
- package/src/merchant/transactions/transaction-history.js +275 -257
- package/src/payment-institution/transactions/create-pi-transaction.js +2 -10
- package/src/payment-institution/transactions/finalize-transaction.js +333 -340
- package/src/payment-institution/transactions/get-transaction-history.js +2 -10
- package/src/payment-institution/transactions/get-transaction-status.js +38 -11
- package/src/payment-institution/transactions/process-pi-transaction.js +2 -10
- package/src/payment-institution/transactions/simulate-pi-callback.js +49 -9
- package/src/payment-institution/transactions/soundbite-transaction.js +2 -10
- package/src/shared/api-errors.js +188 -0
- package/src/shared/auth-headers.js +62 -0
- package/src/shared/data/data-api.js +11 -10
- package/src/shared/deep-links/api.js +5 -4
- package/src/shared/logs/logs-api.js +242 -0
- package/src/shared/poi/poi-create-api.js +419 -0
- package/src/shared/products/products-api.js +17 -16
- package/src/shared/shops/shops-api.js +155 -0
- package/src/tools/advanced-code-generator.js +2255 -2246
- package/src/utils/code-examples/common/auth-examples.js +1484 -1481
- package/src/utils/code-examples/common/helpers.js +7 -4
- package/src/utils/code-examples/isv/oauth2.js +458 -600
- package/src/utils/code-examples/isv/transactions.js +815 -812
- package/MCP-SETUP.md +0 -190
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createJWTToken } from '../../core/auth/jwt-token.js';
|
|
3
|
+
import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
|
|
4
|
+
import { getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
|
|
5
|
+
import { describeApiError } from '../api-errors.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Logs API - Premium Feature
|
|
9
|
+
* Provides access to application logs for debugging and monitoring.
|
|
10
|
+
* Requires Premium (PRM) plan subscription.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Available log levels for filtering
|
|
15
|
+
*/
|
|
16
|
+
export const LOG_LEVELS = {
|
|
17
|
+
ERROR: 'ERROR',
|
|
18
|
+
WARN: 'WARN',
|
|
19
|
+
INFO: 'INFO',
|
|
20
|
+
DEBUG: 'DEBUG'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create authentication headers for logs API
|
|
25
|
+
* @param {string} partnerType - merchant, isv, or payment_institution
|
|
26
|
+
* @param {Object} authParams - Authentication parameters
|
|
27
|
+
* @returns {Object} Headers for API request
|
|
28
|
+
*/
|
|
29
|
+
async function createAuthHeaders(partnerType, authParams) {
|
|
30
|
+
const baseHeaders = {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'Api-Version': '1'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let tokenData;
|
|
36
|
+
|
|
37
|
+
switch (partnerType) {
|
|
38
|
+
case 'merchant':
|
|
39
|
+
case 'payment_institution':
|
|
40
|
+
// Standard JWT authentication
|
|
41
|
+
tokenData = createJWTToken(
|
|
42
|
+
authParams.partnerId,
|
|
43
|
+
authParams.privateKey,
|
|
44
|
+
authParams.requestBody
|
|
45
|
+
);
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case 'isv':
|
|
49
|
+
// ISV JWT with OAuth2
|
|
50
|
+
tokenData = await createJWTForPartner({
|
|
51
|
+
partnerId: authParams.isvPartnerId,
|
|
52
|
+
privateKey: authParams.privateKey,
|
|
53
|
+
merchantId: authParams.merchantPartnerId,
|
|
54
|
+
oauth2Token: authParams.oauth2Token,
|
|
55
|
+
requestBody: authParams.requestBody
|
|
56
|
+
});
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`Unknown partner type: ${partnerType}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...baseHeaders,
|
|
65
|
+
'Authorization': `Bearer ${tokenData.token}`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handle 403 errors with Premium plan messaging
|
|
71
|
+
*/
|
|
72
|
+
function handleLogsError(error, operation) {
|
|
73
|
+
if (error.response?.status === 403) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Access denied: Logs API requires Premium (PRM) plan. ` +
|
|
76
|
+
`Please upgrade your subscription to access application logs. ` +
|
|
77
|
+
`Original error: ${error.response?.data?.message || 'Forbidden'}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
throw describeApiError(error, '${operation}');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Query logs with filters
|
|
85
|
+
* @param {Object} params - Query parameters
|
|
86
|
+
* @returns {Promise<Object>} Log query response
|
|
87
|
+
*/
|
|
88
|
+
export async function queryLogs({
|
|
89
|
+
from,
|
|
90
|
+
to,
|
|
91
|
+
level,
|
|
92
|
+
logger,
|
|
93
|
+
search,
|
|
94
|
+
limit = 1000,
|
|
95
|
+
cursor,
|
|
96
|
+
partnerType,
|
|
97
|
+
...authParams
|
|
98
|
+
}) {
|
|
99
|
+
const headers = await createAuthHeaders(partnerType, authParams);
|
|
100
|
+
|
|
101
|
+
// Build query parameters
|
|
102
|
+
const queryParams = new URLSearchParams();
|
|
103
|
+
if (from) queryParams.append('from', from);
|
|
104
|
+
if (to) queryParams.append('to', to);
|
|
105
|
+
if (level) queryParams.append('level', level);
|
|
106
|
+
if (logger) queryParams.append('logger', logger);
|
|
107
|
+
if (search) queryParams.append('search', search);
|
|
108
|
+
if (limit) queryParams.append('limit', limit.toString());
|
|
109
|
+
if (cursor) queryParams.append('cursor', cursor);
|
|
110
|
+
|
|
111
|
+
const queryString = queryParams.toString();
|
|
112
|
+
const url = `${getProductionUrl()}/logs${queryString ? '?' + queryString : ''}`;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await axios.get(url, { headers });
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
data: response.data,
|
|
120
|
+
requestInfo: {
|
|
121
|
+
url,
|
|
122
|
+
method: 'GET',
|
|
123
|
+
statusCode: response.status
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
handleLogsError(error, 'query logs');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get log status (availability and storage info)
|
|
133
|
+
* @param {Object} params - Authentication parameters
|
|
134
|
+
* @returns {Promise<Object>} Log status response
|
|
135
|
+
*/
|
|
136
|
+
export async function getLogStatus({
|
|
137
|
+
partnerType,
|
|
138
|
+
...authParams
|
|
139
|
+
}) {
|
|
140
|
+
const headers = await createAuthHeaders(partnerType, authParams);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const response = await axios.get(
|
|
144
|
+
`${getProductionUrl()}/logs/status`,
|
|
145
|
+
{ headers }
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
data: response.data,
|
|
151
|
+
requestInfo: {
|
|
152
|
+
url: `${getProductionUrl()}/logs/status`,
|
|
153
|
+
method: 'GET',
|
|
154
|
+
statusCode: response.status
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
handleLogsError(error, 'get log status');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get download URL for logs archive
|
|
164
|
+
* @param {Object} params - Query parameters for date range
|
|
165
|
+
* @returns {Promise<Object>} Download URL information
|
|
166
|
+
*/
|
|
167
|
+
export async function getLogDownloadUrl({
|
|
168
|
+
from,
|
|
169
|
+
to,
|
|
170
|
+
partnerType,
|
|
171
|
+
...authParams
|
|
172
|
+
}) {
|
|
173
|
+
// Build query parameters for the download URL
|
|
174
|
+
const queryParams = new URLSearchParams();
|
|
175
|
+
if (from) queryParams.append('from', from);
|
|
176
|
+
if (to) queryParams.append('to', to);
|
|
177
|
+
|
|
178
|
+
const queryString = queryParams.toString();
|
|
179
|
+
const downloadUrl = `${getProductionUrl()}/logs/download${queryString ? '?' + queryString : ''}`;
|
|
180
|
+
|
|
181
|
+
// Note: The actual download requires authentication headers
|
|
182
|
+
// This returns the URL structure that would be used
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
data: {
|
|
186
|
+
downloadUrl,
|
|
187
|
+
note: 'Use this URL with proper JWT authentication headers to download logs as gzipped JSON',
|
|
188
|
+
parameters: {
|
|
189
|
+
from: from || 'default (24 hours ago)',
|
|
190
|
+
to: to || 'default (now)'
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
requestInfo: {
|
|
194
|
+
url: downloadUrl,
|
|
195
|
+
method: 'GET (with auth headers)'
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Download logs as compressed archive
|
|
202
|
+
* @param {Object} params - Query parameters for date range
|
|
203
|
+
* @returns {Promise<Object>} Downloaded log data
|
|
204
|
+
*/
|
|
205
|
+
export async function downloadLogs({
|
|
206
|
+
from,
|
|
207
|
+
to,
|
|
208
|
+
partnerType,
|
|
209
|
+
...authParams
|
|
210
|
+
}) {
|
|
211
|
+
const headers = await createAuthHeaders(partnerType, authParams);
|
|
212
|
+
|
|
213
|
+
// Build query parameters
|
|
214
|
+
const queryParams = new URLSearchParams();
|
|
215
|
+
if (from) queryParams.append('from', from);
|
|
216
|
+
if (to) queryParams.append('to', to);
|
|
217
|
+
|
|
218
|
+
const queryString = queryParams.toString();
|
|
219
|
+
const url = `${getProductionUrl()}/logs/download${queryString ? '?' + queryString : ''}`;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const response = await axios.get(url, {
|
|
223
|
+
headers,
|
|
224
|
+
responseType: 'arraybuffer'
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
data: response.data,
|
|
230
|
+
contentType: response.headers['content-type'],
|
|
231
|
+
contentDisposition: response.headers['content-disposition'],
|
|
232
|
+
contentLength: response.headers['content-length'],
|
|
233
|
+
requestInfo: {
|
|
234
|
+
url,
|
|
235
|
+
method: 'GET',
|
|
236
|
+
statusCode: response.status
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
handleLogsError(error, 'download logs');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createAuthHeaders } from '../auth-headers.js';
|
|
3
|
+
import { apiErrorResult } from '../api-errors.js';
|
|
4
|
+
import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
|
|
5
|
+
import { getSandboxUrl, getProductionUrl } from '../../config/env.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creating POIs (Points of Interaction) - the physical payment points a payer scans.
|
|
9
|
+
*
|
|
10
|
+
* The existing POI tools could list, read, price and un-price a POI but not create one, so a fleet
|
|
11
|
+
* had to be built by hand in the portal before any of them were usable. `POST /poi` and
|
|
12
|
+
* `POST /poi/batch` close that: batch is the one a terminal rollout actually needs, since it creates
|
|
13
|
+
* up to 50 in a single atomic call.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The server's cap, from POIService.MAX_BATCH_SIZE. Exceeding it is ERR_BATCH_TOO_LARGE. */
|
|
17
|
+
export const MAX_POI_BATCH_SIZE = 50;
|
|
18
|
+
|
|
19
|
+
/** TTL bounds the server validates on both create paths. */
|
|
20
|
+
export const POI_TTL_MIN_SECONDS = 60;
|
|
21
|
+
export const POI_TTL_MAX_SECONDS = 600;
|
|
22
|
+
|
|
23
|
+
function validateTtl(ttlSeconds, label = 'ttlSeconds') {
|
|
24
|
+
if (ttlSeconds === undefined || ttlSeconds === null) return;
|
|
25
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds < POI_TTL_MIN_SECONDS || ttlSeconds > POI_TTL_MAX_SECONDS) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`${label} must be a whole number of seconds between ${POI_TTL_MIN_SECONDS} and ${POI_TTL_MAX_SECONDS}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a single POI.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} params
|
|
36
|
+
* @param {string} params.partnerType 'merchant' | 'isv'
|
|
37
|
+
* @param {string} params.shopCode required - the outlet this POI belongs to
|
|
38
|
+
* @param {string} [params.name] max 100 chars
|
|
39
|
+
* @param {number} [params.ttlSeconds] 60-600, server default 300
|
|
40
|
+
* @param {string} [params.callbackUrl]
|
|
41
|
+
*/
|
|
42
|
+
export async function createPOI({
|
|
43
|
+
partnerType,
|
|
44
|
+
shopCode,
|
|
45
|
+
name,
|
|
46
|
+
ttlSeconds,
|
|
47
|
+
callbackUrl,
|
|
48
|
+
useSandbox = true,
|
|
49
|
+
...authParams
|
|
50
|
+
}) {
|
|
51
|
+
if (!shopCode) {
|
|
52
|
+
throw new Error('shopCode is required - use payware_shops_list to find a valid one');
|
|
53
|
+
}
|
|
54
|
+
if (name !== undefined && String(name).length > 100) {
|
|
55
|
+
throw new Error('POI name must not exceed 100 characters');
|
|
56
|
+
}
|
|
57
|
+
validateTtl(ttlSeconds);
|
|
58
|
+
|
|
59
|
+
const requestBody = {
|
|
60
|
+
shopCode,
|
|
61
|
+
...(name && { name }),
|
|
62
|
+
...(ttlSeconds !== undefined && ttlSeconds !== null && { ttlSeconds }),
|
|
63
|
+
...(callbackUrl && { callbackUrl })
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const minimizedBody = createMinimizedJSON(requestBody);
|
|
67
|
+
const headers = await createAuthHeaders(partnerType, { ...authParams, requestBody: minimizedBody });
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
|
|
71
|
+
const response = await axios.post(`${baseUrl}/poi`, minimizedBody, {
|
|
72
|
+
headers,
|
|
73
|
+
transformRequest: [(data) => data]
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
poi: response.data,
|
|
79
|
+
requestId: response.headers['x-request-id'],
|
|
80
|
+
timestamp: new Date().toISOString()
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return apiErrorResult(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create up to 50 POIs in one atomic call.
|
|
89
|
+
*
|
|
90
|
+
* `shopCode`, `ttlSeconds` and `callbackUrl` set at the top level are the defaults for every entry;
|
|
91
|
+
* an entry may override `name`, `ttlSeconds` and `callbackUrl` for itself.
|
|
92
|
+
*/
|
|
93
|
+
export async function createPOIBatch({
|
|
94
|
+
partnerType,
|
|
95
|
+
shopCode,
|
|
96
|
+
pois,
|
|
97
|
+
ttlSeconds,
|
|
98
|
+
callbackUrl,
|
|
99
|
+
useSandbox = true,
|
|
100
|
+
...authParams
|
|
101
|
+
}) {
|
|
102
|
+
if (!shopCode) {
|
|
103
|
+
throw new Error('shopCode is required - use payware_shops_list to find a valid one');
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(pois) || pois.length === 0) {
|
|
106
|
+
// Matches the server's ERR_BATCH_EMPTY, refused here so the round trip is not wasted.
|
|
107
|
+
throw new Error('pois must be a non-empty array (server: ERR_BATCH_EMPTY)');
|
|
108
|
+
}
|
|
109
|
+
if (pois.length > MAX_POI_BATCH_SIZE) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`pois must contain at most ${MAX_POI_BATCH_SIZE} entries (server: ERR_BATCH_TOO_LARGE). ` +
|
|
112
|
+
`Received ${pois.length} - split the rollout into batches of ${MAX_POI_BATCH_SIZE}.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
validateTtl(ttlSeconds);
|
|
116
|
+
pois.forEach((poi, i) => {
|
|
117
|
+
if (poi?.name !== undefined && String(poi.name).length > 100) {
|
|
118
|
+
throw new Error(`pois[${i}].name must not exceed 100 characters`);
|
|
119
|
+
}
|
|
120
|
+
validateTtl(poi?.ttlSeconds, `pois[${i}].ttlSeconds`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const requestBody = {
|
|
124
|
+
shopCode,
|
|
125
|
+
...(ttlSeconds !== undefined && ttlSeconds !== null && { ttlSeconds }),
|
|
126
|
+
...(callbackUrl && { callbackUrl }),
|
|
127
|
+
pois: pois.map(poi => ({
|
|
128
|
+
...(poi.name && { name: poi.name }),
|
|
129
|
+
...(poi.ttlSeconds !== undefined && poi.ttlSeconds !== null && { ttlSeconds: poi.ttlSeconds }),
|
|
130
|
+
...(poi.callbackUrl && { callbackUrl: poi.callbackUrl })
|
|
131
|
+
}))
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const minimizedBody = createMinimizedJSON(requestBody);
|
|
135
|
+
const headers = await createAuthHeaders(partnerType, { ...authParams, requestBody: minimizedBody });
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
|
|
139
|
+
const response = await axios.post(`${baseUrl}/poi/batch`, minimizedBody, {
|
|
140
|
+
headers,
|
|
141
|
+
transformRequest: [(data) => data]
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
success: true,
|
|
146
|
+
created: response.data?.created ?? 0,
|
|
147
|
+
pois: response.data?.pois ?? [],
|
|
148
|
+
requestId: response.headers['x-request-id'],
|
|
149
|
+
timestamp: new Date().toISOString()
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return apiErrorResult(error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const SHARED_POI_NOTES = `
|
|
157
|
+
**A POI belongs to a shop, and a shop belongs to a merchant.** \`shopCode\` is required - use
|
|
158
|
+
\`payware_shops_list\` to find one rather than guessing. Banks do not own POIs; a financial-institution
|
|
159
|
+
principal gets 403 on every POI endpoint, not an empty result.
|
|
160
|
+
|
|
161
|
+
**\`ttlSeconds\` is the price-session TTL, not the POI's lifetime.** It bounds how long a price set on
|
|
162
|
+
this POI stays claimable before the POI returns to IDLE. Range ${POI_TTL_MIN_SECONDS}-${POI_TTL_MAX_SECONDS} seconds; the server
|
|
163
|
+
defaults it to 300.`;
|
|
164
|
+
|
|
165
|
+
function errorText(title, result) {
|
|
166
|
+
return `❌ **${title}**
|
|
167
|
+
|
|
168
|
+
- Message: ${result.error.message}
|
|
169
|
+
- Status: ${result.error.status || 'N/A'}
|
|
170
|
+
- Code: ${result.error.errorCode || 'N/A'}
|
|
171
|
+
${result.error.correlationId ? `- Correlation ID: ${result.error.correlationId}` : ''}
|
|
172
|
+
${result.error.guidance ? `\n${result.error.guidance}` : ''}
|
|
173
|
+
|
|
174
|
+
**Timestamp:** ${result.timestamp}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isvAuthProperties() {
|
|
178
|
+
return {
|
|
179
|
+
merchantPartnerId: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
description: 'Partner ID of the target merchant (8 alphanumeric characters)'
|
|
182
|
+
},
|
|
183
|
+
oauth2Token: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: 'OAuth2 access token obtained from the merchant'
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function merchantAuthProperties() {
|
|
191
|
+
return {
|
|
192
|
+
partnerId: {
|
|
193
|
+
type: 'string',
|
|
194
|
+
description: 'Partner ID from the payware dashboard. Uses PAYWARE_PARTNER_ID env var as default.'
|
|
195
|
+
},
|
|
196
|
+
privateKey: {
|
|
197
|
+
type: 'string',
|
|
198
|
+
description: 'RSA private key for JWT signing. Uses the environment-specific key as default.'
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Build the single-POI create tool for a role. */
|
|
204
|
+
export function buildCreatePOITool({ partnerType, resolveAuth }) {
|
|
205
|
+
const isIsv = partnerType === 'isv';
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
name: 'payware_poi_create',
|
|
209
|
+
description: `Create a single POI (Point of Interaction) - a physical payment point a payer scans.
|
|
210
|
+
|
|
211
|
+
**Endpoint:** POST /poi -> 201 Created
|
|
212
|
+
${SHARED_POI_NOTES}
|
|
213
|
+
|
|
214
|
+
**Creating many at once?** Use \`payware_poi_create_batch\` instead. It creates up to ${MAX_POI_BATCH_SIZE} in one
|
|
215
|
+
atomic call, which is both faster and safer than a loop over this tool: a loop that fails halfway
|
|
216
|
+
leaves a half-built estate with no record of where it stopped.
|
|
217
|
+
${isIsv ? `
|
|
218
|
+
⚠️ **ISV scope and plan.** The shop must be in the scope this merchant assigned you, or the call is
|
|
219
|
+
**403 \`ERR_SHOP_NOT_IN_SCOPE\`**. The acting merchant's plan governs, not yours.
|
|
220
|
+
` : ''}`,
|
|
221
|
+
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
required: isIsv ? ['merchantPartnerId', 'oauth2Token', 'shopCode'] : ['shopCode'],
|
|
225
|
+
properties: {
|
|
226
|
+
...(isIsv ? isvAuthProperties() : merchantAuthProperties()),
|
|
227
|
+
shopCode: {
|
|
228
|
+
type: 'string',
|
|
229
|
+
description: 'Code of the shop this POI belongs to. Required. Find one with payware_shops_list.'
|
|
230
|
+
},
|
|
231
|
+
name: {
|
|
232
|
+
type: 'string',
|
|
233
|
+
description: 'Human-readable POI name, e.g. "Till 3" or "Gate B". Shown in the portal and in POI listings.',
|
|
234
|
+
maxLength: 100
|
|
235
|
+
},
|
|
236
|
+
ttlSeconds: {
|
|
237
|
+
type: 'integer',
|
|
238
|
+
description: `Price-session TTL in seconds - how long a price set on this POI stays claimable before it returns to IDLE. Server default 300.`,
|
|
239
|
+
minimum: POI_TTL_MIN_SECONDS,
|
|
240
|
+
maximum: POI_TTL_MAX_SECONDS
|
|
241
|
+
},
|
|
242
|
+
callbackUrl: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
description: 'HTTPS URL for POI events. Must present a publicly-trusted TLS certificate and resolve to a public address.',
|
|
245
|
+
format: 'uri'
|
|
246
|
+
},
|
|
247
|
+
useSandbox: {
|
|
248
|
+
type: 'boolean',
|
|
249
|
+
description: 'Use sandbox environment for testing',
|
|
250
|
+
default: true
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
async handler(args) {
|
|
256
|
+
const { shopCode, name, ttlSeconds, callbackUrl, useSandbox = true } = args;
|
|
257
|
+
const result = await createPOI({
|
|
258
|
+
partnerType,
|
|
259
|
+
shopCode,
|
|
260
|
+
name,
|
|
261
|
+
ttlSeconds,
|
|
262
|
+
callbackUrl,
|
|
263
|
+
useSandbox,
|
|
264
|
+
...resolveAuth(args)
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (!result.success) {
|
|
268
|
+
return { content: [{ type: 'text', text: errorText('Failed to Create POI', result) }] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const poi = result.poi || {};
|
|
272
|
+
return {
|
|
273
|
+
content: [{
|
|
274
|
+
type: 'text',
|
|
275
|
+
text: `✅ **POI Created**
|
|
276
|
+
|
|
277
|
+
- POI ID: \`${poi.poiId}\`
|
|
278
|
+
- Name: ${poi.name || '(unnamed)'}
|
|
279
|
+
- Shop: ${poi.shopName || poi.shopCode || shopCode}
|
|
280
|
+
- Status: ${poi.status || 'IDLE'}
|
|
281
|
+
|
|
282
|
+
**Next:** set a price with \`payware_poi_set_price\`, or fetch its QR code with
|
|
283
|
+
\`payware_poi_get_qrcode\` to print and mount at the payment point.
|
|
284
|
+
|
|
285
|
+
**Request ID:** ${result.requestId || 'N/A'}
|
|
286
|
+
**Timestamp:** ${result.timestamp}`
|
|
287
|
+
}]
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Build the batch-create tool for a role. */
|
|
294
|
+
export function buildCreatePOIBatchTool({ partnerType, resolveAuth }) {
|
|
295
|
+
const isIsv = partnerType === 'isv';
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
name: 'payware_poi_create_batch',
|
|
299
|
+
description: `Create up to ${MAX_POI_BATCH_SIZE} POIs in a single **atomic** call - the tool for rolling out a terminal estate.
|
|
300
|
+
|
|
301
|
+
**Endpoint:** POST /poi/batch -> 201 Created, returning \`{ created, pois: [...] }\`
|
|
302
|
+
|
|
303
|
+
**Atomic means all or nothing.** Either every entry is created or none is. That is the reason to
|
|
304
|
+
prefer this over a loop of \`payware_poi_create\`: a loop that fails on entry 27 leaves 26 POIs behind
|
|
305
|
+
with nothing recording where it stopped, and re-running it duplicates them.
|
|
306
|
+
${SHARED_POI_NOTES}
|
|
307
|
+
|
|
308
|
+
**Defaults and overrides.** \`shopCode\`, \`ttlSeconds\` and \`callbackUrl\` at the top level apply to
|
|
309
|
+
every entry. An entry may override \`name\`, \`ttlSeconds\` and \`callbackUrl\` for itself. \`shopCode\` is
|
|
310
|
+
top-level only - one batch builds one shop.
|
|
311
|
+
|
|
312
|
+
**Limits:** 1 to ${MAX_POI_BATCH_SIZE} entries. An empty list is \`ERR_BATCH_EMPTY\`; more than ${MAX_POI_BATCH_SIZE} is
|
|
313
|
+
\`ERR_BATCH_TOO_LARGE\`. Both are refused locally before the request is sent. For a larger rollout,
|
|
314
|
+
split it into batches of ${MAX_POI_BATCH_SIZE} - each batch is atomic on its own, so record which batches succeeded.
|
|
315
|
+
${isIsv ? `
|
|
316
|
+
⚠️ **ISV scope and plan.** The shop must be in the scope this merchant assigned you, or the whole
|
|
317
|
+
batch is **403 \`ERR_SHOP_NOT_IN_SCOPE\`**. The acting merchant's plan governs, not yours.
|
|
318
|
+
` : ''}`,
|
|
319
|
+
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
required: isIsv ? ['merchantPartnerId', 'oauth2Token', 'shopCode', 'pois'] : ['shopCode', 'pois'],
|
|
323
|
+
properties: {
|
|
324
|
+
...(isIsv ? isvAuthProperties() : merchantAuthProperties()),
|
|
325
|
+
shopCode: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description: 'Code of the shop every POI in this batch belongs to. Required, top-level only. Find one with payware_shops_list.'
|
|
328
|
+
},
|
|
329
|
+
pois: {
|
|
330
|
+
type: 'array',
|
|
331
|
+
description: `The POIs to create. Each entry may set name, ttlSeconds and callbackUrl; anything omitted falls back to the top-level value. An entry may be an empty object, which creates an unnamed POI with the batch defaults.`,
|
|
332
|
+
minItems: 1,
|
|
333
|
+
maxItems: MAX_POI_BATCH_SIZE,
|
|
334
|
+
items: {
|
|
335
|
+
type: 'object',
|
|
336
|
+
properties: {
|
|
337
|
+
name: {
|
|
338
|
+
type: 'string',
|
|
339
|
+
description: 'Human-readable POI name, e.g. "Till 3".',
|
|
340
|
+
maxLength: 100
|
|
341
|
+
},
|
|
342
|
+
ttlSeconds: {
|
|
343
|
+
type: 'integer',
|
|
344
|
+
description: 'Overrides the batch ttlSeconds for this POI.',
|
|
345
|
+
minimum: POI_TTL_MIN_SECONDS,
|
|
346
|
+
maximum: POI_TTL_MAX_SECONDS
|
|
347
|
+
},
|
|
348
|
+
callbackUrl: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
description: 'Overrides the batch callbackUrl for this POI.',
|
|
351
|
+
format: 'uri'
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
ttlSeconds: {
|
|
357
|
+
type: 'integer',
|
|
358
|
+
description: 'Default price-session TTL for every POI in the batch. Server default 300.',
|
|
359
|
+
minimum: POI_TTL_MIN_SECONDS,
|
|
360
|
+
maximum: POI_TTL_MAX_SECONDS
|
|
361
|
+
},
|
|
362
|
+
callbackUrl: {
|
|
363
|
+
type: 'string',
|
|
364
|
+
description: 'Default HTTPS callback URL for every POI in the batch.',
|
|
365
|
+
format: 'uri'
|
|
366
|
+
},
|
|
367
|
+
useSandbox: {
|
|
368
|
+
type: 'boolean',
|
|
369
|
+
description: 'Use sandbox environment for testing',
|
|
370
|
+
default: true
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
async handler(args) {
|
|
376
|
+
const { shopCode, pois, ttlSeconds, callbackUrl, useSandbox = true } = args;
|
|
377
|
+
const result = await createPOIBatch({
|
|
378
|
+
partnerType,
|
|
379
|
+
shopCode,
|
|
380
|
+
pois,
|
|
381
|
+
ttlSeconds,
|
|
382
|
+
callbackUrl,
|
|
383
|
+
useSandbox,
|
|
384
|
+
...resolveAuth(args)
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (!result.success) {
|
|
388
|
+
return {
|
|
389
|
+
content: [{
|
|
390
|
+
type: 'text',
|
|
391
|
+
text: `${errorText('Failed to Create POI Batch', result)}
|
|
392
|
+
|
|
393
|
+
**Nothing was created.** The call is atomic, so a failure leaves no partial estate behind - fix the
|
|
394
|
+
cause and re-send the same batch.`
|
|
395
|
+
}]
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const created = result.pois.map(poi =>
|
|
400
|
+
`- \`${poi.poiId}\` ${poi.name || '(unnamed)'} - ${poi.status || 'IDLE'}`
|
|
401
|
+
).join('\n');
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
content: [{
|
|
405
|
+
type: 'text',
|
|
406
|
+
text: `✅ **POI Batch Created** - ${result.created} POI(s) in shop \`${shopCode}\`
|
|
407
|
+
|
|
408
|
+
${created}
|
|
409
|
+
|
|
410
|
+
**Next:** fetch each QR code with \`payware_poi_get_qrcode\` to print and mount, then set prices with
|
|
411
|
+
\`payware_poi_set_price\` as sales happen.
|
|
412
|
+
|
|
413
|
+
**Request ID:** ${result.requestId || 'N/A'}
|
|
414
|
+
**Timestamp:** ${result.timestamp}`
|
|
415
|
+
}]
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|