@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,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
|
+
}
|
|
@@ -4,6 +4,7 @@ import { createJWTToken } from '../../core/auth/jwt-token.js';
|
|
|
4
4
|
import { createJWTForPartner } from '../../core/auth/jwt-factory.js';
|
|
5
5
|
import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
|
|
6
6
|
import { getSandboxUrl, getProductionUrl } from '../../config/env.js';
|
|
7
|
+
import { describeApiError } from '../api-errors.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Product types
|
|
@@ -192,7 +193,7 @@ export async function createProduct({
|
|
|
192
193
|
}
|
|
193
194
|
};
|
|
194
195
|
} catch (error) {
|
|
195
|
-
throw
|
|
196
|
+
throw describeApiError(error, 'create product');
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -223,7 +224,7 @@ export async function getProducts({
|
|
|
223
224
|
}
|
|
224
225
|
};
|
|
225
226
|
} catch (error) {
|
|
226
|
-
throw
|
|
227
|
+
throw describeApiError(error, 'get products');
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
@@ -259,7 +260,7 @@ export async function getProduct({
|
|
|
259
260
|
}
|
|
260
261
|
};
|
|
261
262
|
} catch (error) {
|
|
262
|
-
throw
|
|
263
|
+
throw describeApiError(error, 'get product');
|
|
263
264
|
}
|
|
264
265
|
}
|
|
265
266
|
|
|
@@ -322,7 +323,7 @@ export async function updateProduct({
|
|
|
322
323
|
}
|
|
323
324
|
};
|
|
324
325
|
} catch (error) {
|
|
325
|
-
throw
|
|
326
|
+
throw describeApiError(error, 'update product');
|
|
326
327
|
}
|
|
327
328
|
}
|
|
328
329
|
|
|
@@ -357,7 +358,7 @@ export async function deleteProduct({
|
|
|
357
358
|
}
|
|
358
359
|
};
|
|
359
360
|
} catch (error) {
|
|
360
|
-
throw
|
|
361
|
+
throw describeApiError(error, 'delete product');
|
|
361
362
|
}
|
|
362
363
|
}
|
|
363
364
|
|
|
@@ -410,7 +411,7 @@ export async function getProductImage({
|
|
|
410
411
|
}
|
|
411
412
|
};
|
|
412
413
|
} catch (error) {
|
|
413
|
-
throw
|
|
414
|
+
throw describeApiError(error, 'get product image');
|
|
414
415
|
}
|
|
415
416
|
}
|
|
416
417
|
|
|
@@ -469,7 +470,7 @@ export async function createPriceSchedule({
|
|
|
469
470
|
}
|
|
470
471
|
};
|
|
471
472
|
} catch (error) {
|
|
472
|
-
throw
|
|
473
|
+
throw describeApiError(error, 'create price schedule');
|
|
473
474
|
}
|
|
474
475
|
}
|
|
475
476
|
|
|
@@ -505,7 +506,7 @@ export async function getProductSchedules({
|
|
|
505
506
|
}
|
|
506
507
|
};
|
|
507
508
|
} catch (error) {
|
|
508
|
-
throw
|
|
509
|
+
throw describeApiError(error, 'get product schedules');
|
|
509
510
|
}
|
|
510
511
|
}
|
|
511
512
|
|
|
@@ -545,7 +546,7 @@ export async function getSchedule({
|
|
|
545
546
|
}
|
|
546
547
|
};
|
|
547
548
|
} catch (error) {
|
|
548
|
-
throw
|
|
549
|
+
throw describeApiError(error, 'get schedule');
|
|
549
550
|
}
|
|
550
551
|
}
|
|
551
552
|
|
|
@@ -603,7 +604,7 @@ export async function updatePriceSchedule({
|
|
|
603
604
|
}
|
|
604
605
|
};
|
|
605
606
|
} catch (error) {
|
|
606
|
-
throw
|
|
607
|
+
throw describeApiError(error, 'update schedule');
|
|
607
608
|
}
|
|
608
609
|
}
|
|
609
610
|
|
|
@@ -642,7 +643,7 @@ export async function deletePriceSchedule({
|
|
|
642
643
|
}
|
|
643
644
|
};
|
|
644
645
|
} catch (error) {
|
|
645
|
-
throw
|
|
646
|
+
throw describeApiError(error, 'delete schedule');
|
|
646
647
|
}
|
|
647
648
|
}
|
|
648
649
|
|
|
@@ -702,7 +703,7 @@ export async function registerAudio({
|
|
|
702
703
|
}
|
|
703
704
|
};
|
|
704
705
|
} catch (error) {
|
|
705
|
-
throw
|
|
706
|
+
throw describeApiError(error, 'register audio');
|
|
706
707
|
}
|
|
707
708
|
}
|
|
708
709
|
|
|
@@ -738,7 +739,7 @@ export async function getProductAudios({
|
|
|
738
739
|
}
|
|
739
740
|
};
|
|
740
741
|
} catch (error) {
|
|
741
|
-
throw
|
|
742
|
+
throw describeApiError(error, 'get product audios');
|
|
742
743
|
}
|
|
743
744
|
}
|
|
744
745
|
|
|
@@ -778,7 +779,7 @@ export async function getAudio({
|
|
|
778
779
|
}
|
|
779
780
|
};
|
|
780
781
|
} catch (error) {
|
|
781
|
-
throw
|
|
782
|
+
throw describeApiError(error, 'get audio');
|
|
782
783
|
}
|
|
783
784
|
}
|
|
784
785
|
|
|
@@ -828,7 +829,7 @@ export async function updateAudio({
|
|
|
828
829
|
}
|
|
829
830
|
};
|
|
830
831
|
} catch (error) {
|
|
831
|
-
throw
|
|
832
|
+
throw describeApiError(error, 'update audio');
|
|
832
833
|
}
|
|
833
834
|
}
|
|
834
835
|
|
|
@@ -867,6 +868,6 @@ export async function deleteAudio({
|
|
|
867
868
|
}
|
|
868
869
|
};
|
|
869
870
|
} catch (error) {
|
|
870
|
-
throw
|
|
871
|
+
throw describeApiError(error, 'delete audio');
|
|
871
872
|
}
|
|
872
873
|
}
|