@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.
Files changed (58) hide show
  1. package/README.md +5 -0
  2. package/package.json +69 -66
  3. package/src/auth/sandbox-setup.js +186 -186
  4. package/src/isv/index.js +29 -1
  5. package/src/isv/invitations/index.js +471 -0
  6. package/src/isv/oauth2/obtain-token.js +19 -8
  7. package/src/isv/oauth2/token-helper.js +5 -2
  8. package/src/isv/poi/cancel-price.js +2 -10
  9. package/src/isv/poi/create.js +19 -0
  10. package/src/isv/poi/get-poi.js +2 -10
  11. package/src/isv/poi/get-qrcode.js +2 -10
  12. package/src/isv/poi/get-status.js +2 -10
  13. package/src/isv/poi/index.js +5 -0
  14. package/src/isv/poi/list-pois.js +2 -10
  15. package/src/isv/poi/set-price.js +217 -225
  16. package/src/isv/products/create-product.js +313 -313
  17. package/src/isv/products/update-product.js +264 -264
  18. package/src/isv/reference/index.js +249 -0
  19. package/src/isv/shops/index.js +22 -0
  20. package/src/isv/transactions/cancel-transaction.js +41 -10
  21. package/src/isv/transactions/create-transaction.js +20 -4
  22. package/src/isv/transactions/get-status.js +18 -11
  23. package/src/isv/transactions/process-transaction.js +160 -42
  24. package/src/isv/transactions/simulate-callback.js +128 -109
  25. package/src/isv/transactions/transaction-history.js +86 -90
  26. package/src/merchant/data/generate-report.js +19 -1
  27. package/src/merchant/index.js +21 -1
  28. package/src/merchant/logs/index.js +366 -0
  29. package/src/merchant/products/create-product.js +287 -287
  30. package/src/merchant/products/update-product.js +281 -281
  31. package/src/merchant/shops/index.js +18 -0
  32. package/src/merchant/transactions/cancel-transaction.js +10 -13
  33. package/src/merchant/transactions/create-transaction.js +88 -8
  34. package/src/merchant/transactions/get-status.js +40 -11
  35. package/src/merchant/transactions/process-transaction.js +2 -10
  36. package/src/merchant/transactions/simulate-callback.js +68 -9
  37. package/src/merchant/transactions/transaction-history.js +275 -257
  38. package/src/payment-institution/transactions/create-pi-transaction.js +2 -10
  39. package/src/payment-institution/transactions/finalize-transaction.js +333 -340
  40. package/src/payment-institution/transactions/get-transaction-history.js +2 -10
  41. package/src/payment-institution/transactions/get-transaction-status.js +38 -11
  42. package/src/payment-institution/transactions/process-pi-transaction.js +2 -10
  43. package/src/payment-institution/transactions/simulate-pi-callback.js +49 -9
  44. package/src/payment-institution/transactions/soundbite-transaction.js +2 -10
  45. package/src/shared/api-errors.js +188 -0
  46. package/src/shared/auth-headers.js +62 -0
  47. package/src/shared/data/data-api.js +11 -10
  48. package/src/shared/deep-links/api.js +5 -4
  49. package/src/shared/logs/logs-api.js +242 -0
  50. package/src/shared/poi/poi-create-api.js +419 -0
  51. package/src/shared/products/products-api.js +17 -16
  52. package/src/shared/shops/shops-api.js +155 -0
  53. package/src/tools/advanced-code-generator.js +2255 -2246
  54. package/src/utils/code-examples/common/auth-examples.js +1484 -1481
  55. package/src/utils/code-examples/common/helpers.js +7 -4
  56. package/src/utils/code-examples/isv/oauth2.js +458 -600
  57. package/src/utils/code-examples/isv/transactions.js +815 -812
  58. 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
+ }