@payware/mcp-server 1.3.3 → 1.5.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 +4 -0
- package/README.md +18 -0
- package/package.json +7 -9
- package/src/auth/rsa-keys.js +12 -6
- package/src/auth/sandbox-setup.js +2 -0
- package/src/config/toolsets.js +104 -0
- package/src/core/auth/jwt-factory.js +4 -1
- package/src/core/auth/jwt-token.js +4 -0
- package/src/index.js +10 -6
- package/src/isv/data/index.js +15 -14
- package/src/isv/deep-links/delete-product-link.js +3 -2
- package/src/isv/deep-links/delete-transaction-link.js +3 -2
- package/src/isv/deep-links/get-product-link.js +8 -6
- package/src/isv/deep-links/get-transaction-link.js +8 -6
- package/src/isv/index.js +6 -0
- package/src/isv/invitations/index.js +5 -0
- package/src/isv/oauth2/get-token-info.js +3 -0
- package/src/isv/oauth2/obtain-token.js +3 -0
- package/src/isv/oauth2/token-helper.js +2 -0
- package/src/isv/poi/cancel-price.js +2 -0
- package/src/isv/poi/get-poi.js +2 -0
- package/src/isv/poi/get-qrcode.js +40 -9
- package/src/isv/poi/get-status.js +2 -0
- package/src/isv/poi/list-pois.js +2 -0
- package/src/isv/poi/set-price.js +2 -0
- package/src/isv/products/audios.js +8 -2
- package/src/isv/products/create-product.js +2 -0
- package/src/isv/products/delete-product.js +2 -0
- package/src/isv/products/get-product-image.js +2 -0
- package/src/isv/products/get-product.js +2 -0
- package/src/isv/products/list-products.js +2 -0
- package/src/isv/products/schedules.js +6 -0
- package/src/isv/products/update-product.js +2 -0
- package/src/isv/reference/index.js +2 -0
- package/src/isv/transactions/cancel-transaction.js +2 -0
- package/src/isv/transactions/create-transaction.js +14 -2
- package/src/isv/transactions/get-status.js +3 -1
- package/src/isv/transactions/process-transaction.js +14 -2
- package/src/isv/transactions/simulate-callback.js +2 -0
- package/src/isv/transactions/transaction-history.js +2 -0
- package/src/merchant/data/export-report.js +2 -0
- package/src/merchant/data/generate-report.js +2 -0
- package/src/merchant/data/get-export-list.js +2 -0
- package/src/merchant/data/get-report-requests.js +2 -0
- package/src/merchant/data/get-report-status.js +2 -0
- package/src/merchant/data/index.js +15 -10
- package/src/merchant/data/list-reports.js +2 -0
- package/src/merchant/deep-links/delete-product-link.js +3 -2
- package/src/merchant/deep-links/delete-transaction-link.js +3 -2
- package/src/merchant/deep-links/get-product-link.js +8 -6
- package/src/merchant/deep-links/get-transaction-link.js +8 -6
- package/src/merchant/index.js +7 -1
- package/src/merchant/logs/index.js +4 -0
- package/src/merchant/products/audios.js +15 -14
- package/src/merchant/products/create-product.js +3 -2
- package/src/merchant/products/delete-product.js +3 -2
- package/src/merchant/products/get-product-image.js +3 -2
- package/src/merchant/products/get-product.js +3 -2
- package/src/merchant/products/list-products.js +3 -2
- package/src/merchant/products/schedules.js +11 -10
- package/src/merchant/products/update-product.js +3 -2
- package/src/merchant/transactions/cancel-transaction.js +4 -3
- package/src/merchant/transactions/create-transaction.js +14 -2
- package/src/merchant/transactions/get-status.js +4 -3
- package/src/merchant/transactions/process-transaction.js +17 -6
- package/src/merchant/transactions/simulate-callback.js +3 -0
- package/src/merchant/transactions/transaction-history.js +5 -4
- package/src/payment-institution/data/index.js +15 -14
- package/src/payment-institution/deep-links/delete-transaction-link.js +3 -2
- package/src/payment-institution/deep-links/get-transaction-link.js +8 -6
- package/src/payment-institution/transactions/create-pi-transaction.js +15 -4
- package/src/payment-institution/transactions/finalize-transaction.js +7 -4
- package/src/payment-institution/transactions/get-transaction-history.js +7 -4
- package/src/payment-institution/transactions/get-transaction-status.js +15 -12
- package/src/payment-institution/transactions/process-pi-transaction.js +19 -8
- package/src/payment-institution/transactions/simulate-pi-callback.js +3 -0
- package/src/payment-institution/transactions/soundbite-transaction.js +5 -4
- package/src/proxy-server.js +25 -0
- package/src/shared/placements/api.js +51 -0
- package/src/shared/placements/tools.js +519 -0
- package/src/shared/poi/poi-create-api.js +3 -0
- package/src/shared/products/products-api.js +36 -25
- package/src/shared/shops/shops-api.js +2 -0
- package/src/shared/tool-annotations.js +53 -0
- package/src/tools/advanced-code-generator.js +3 -0
- package/src/utils/format-request.js +2 -0
- package/src/utils/json-formatter.js +2 -0
- package/src/utils/server-info.js +2 -0
- package/src/utils/code-examples.js +0 -646
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
|
|
2
|
+
import { ADDS, CHANGES, CREATES, READS } from '../tool-annotations.js';
|
|
3
|
+
import { placementsRequest } from './api.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Campaigns, placements and airings, for a merchant and for an ISV acting for one.
|
|
7
|
+
*
|
|
8
|
+
* One definition per tool, built for either role by `placementTools(role)`. The two roles differ only
|
|
9
|
+
* in how they authenticate and in what an ISV may see, so writing each of the 22 tools twice, as the
|
|
10
|
+
* older product modules do, would be 22 chances for the copies to drift. The server routes are on
|
|
11
|
+
* branch feature/placements: CampaignController, PlacementController, AiringController and
|
|
12
|
+
* PlacementCalendarController; tests/tool-routes.test.js pins each tool to its route.
|
|
13
|
+
*
|
|
14
|
+
* Credentials come from the environment only. Unlike the older tools these take no `partnerId` or
|
|
15
|
+
* `privateKey` argument: a key passed as an argument travels through the model's context.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const USE_SANDBOX = {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
description: 'Use the sandbox (the default) rather than production',
|
|
21
|
+
default: true
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ROLES = {
|
|
25
|
+
merchant: {
|
|
26
|
+
properties: {},
|
|
27
|
+
required: [],
|
|
28
|
+
note: '',
|
|
29
|
+
auth(args) {
|
|
30
|
+
const useSandbox = args.useSandbox ?? true;
|
|
31
|
+
const partnerId = getPartnerIdSafe();
|
|
32
|
+
const privateKey = getPrivateKeySafe(useSandbox);
|
|
33
|
+
if (!partnerId || !privateKey) {
|
|
34
|
+
throw new Error('Partner credentials are not configured: set PAYWARE_PARTNER_ID and the private key path for this environment.');
|
|
35
|
+
}
|
|
36
|
+
return { partnerType: 'merchant', partnerId, privateKey, useSandbox };
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
isv: {
|
|
40
|
+
properties: {
|
|
41
|
+
merchantPartnerId: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: "The merchant you act for: its 8-character partner ID, e.g. 'PZAYNMVE'"
|
|
44
|
+
},
|
|
45
|
+
oauth2Token: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'The OAuth2 access token the merchant granted you'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
required: ['merchantPartnerId', 'oauth2Token'],
|
|
51
|
+
note: "\n\n**Acting for a merchant:** you see and manage the campaigns you created and the merchant's campaigns that hold placements in your shops, and only placements of products in your shops. The merchant's plan applies, not yours.",
|
|
52
|
+
auth(args) {
|
|
53
|
+
const useSandbox = args.useSandbox ?? true;
|
|
54
|
+
const isvPartnerId = getPartnerIdSafe();
|
|
55
|
+
const privateKey = getPrivateKeySafe(useSandbox);
|
|
56
|
+
if (!isvPartnerId || !privateKey) {
|
|
57
|
+
throw new Error('ISV credentials are not configured: set PAYWARE_PARTNER_ID and the private key path for this environment.');
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
partnerType: 'isv', isvPartnerId, privateKey, useSandbox,
|
|
61
|
+
merchantPartnerId: args.merchantPartnerId, oauth2Token: args.oauth2Token
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// --- Parameters ----------------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const PRODUCT_ID = {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: "The product's ID: pr + 8 letters or digits, e.g. pr7e4rCToG",
|
|
72
|
+
pattern: '^pr[0-9A-Za-z]{8}$'
|
|
73
|
+
};
|
|
74
|
+
const PLACEMENT_ID = {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: "The placement's ID: pl + 8 letters or digits, e.g. pl7e4rCToG",
|
|
77
|
+
pattern: '^pl[0-9A-Za-z]{8}$'
|
|
78
|
+
};
|
|
79
|
+
const CAMPAIGN_ID = {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: "The campaign's ID: cp + 8 letters or digits, e.g. cp7e4rCToG",
|
|
82
|
+
pattern: '^cp[0-9A-Za-z]{8}$'
|
|
83
|
+
};
|
|
84
|
+
const RULE_ID = {
|
|
85
|
+
type: 'integer',
|
|
86
|
+
description: "The airing rule's ID, from payware_products_list_airings",
|
|
87
|
+
minimum: 1
|
|
88
|
+
};
|
|
89
|
+
const DATE = { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$' };
|
|
90
|
+
const RANGE_FORMS = 'a date (YYYY-MM-DD, the start of that day in UTC) or an ISO-8601 instant with an offset, e.g. 2026-10-19T00:00:00+03:00';
|
|
91
|
+
|
|
92
|
+
const STATS_RANGE = {
|
|
93
|
+
from: { type: 'string', description: `Start of the period: ${RANGE_FORMS}. Default: 30 days before to` },
|
|
94
|
+
to: { type: 'string', description: `End of the period, exclusive, in the same forms. Default: now. At most 400 days after from` }
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const CAMPAIGN_FIELDS = {
|
|
98
|
+
name: { type: 'string', description: 'Campaign name, up to 100 characters', maxLength: 100 },
|
|
99
|
+
startsOn: { ...DATE, description: 'First day, YYYY-MM-DD' },
|
|
100
|
+
endsOn: { ...DATE, description: 'Last day, YYYY-MM-DD' },
|
|
101
|
+
status: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
enum: ['DRAFT', 'ACTIVE', 'ENDED'],
|
|
104
|
+
description: 'Descriptive only: it does not switch the placements on or off'
|
|
105
|
+
},
|
|
106
|
+
externalRef: { type: 'string', description: "An agency's own campaign code, up to 100 characters", maxLength: 100 }
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const MEDIA = ['PRINT', 'PACKAGING', 'OUTDOOR', 'IN_STORE', 'EVENT', 'TV', 'RADIO', 'STREAMING', 'ONLINE', 'SOCIAL', 'EMAIL', 'SMS', 'OTHER'];
|
|
110
|
+
const CARRIERS = ['QR', 'BARCODE', 'NFC', 'BLE', 'LINK', 'SMS', 'SOUNDBITE'];
|
|
111
|
+
|
|
112
|
+
const PLACEMENT_FIELDS = {
|
|
113
|
+
name: { type: 'string', description: 'Placement name, up to 100 characters, e.g. "Sofia metro posters, October"', maxLength: 100 },
|
|
114
|
+
campaignId: { ...CAMPAIGN_ID, description: 'The campaign it belongs to, if any (cp + 8 characters)' },
|
|
115
|
+
medium: { type: 'string', enum: MEDIA, description: 'What the placement is' },
|
|
116
|
+
carriers: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
items: { type: 'string', enum: CARRIERS },
|
|
119
|
+
minItems: 1,
|
|
120
|
+
maxItems: 7,
|
|
121
|
+
description: 'The methods that carry the placement ID to a payer. SOUNDBITE airs the product\'s audio and needs airing rules'
|
|
122
|
+
},
|
|
123
|
+
channel: { type: 'string', description: 'Descriptive, e.g. the TV channel or the website, up to 100 characters', maxLength: 100 },
|
|
124
|
+
location: { type: 'string', description: 'Descriptive, e.g. the venue or the address, up to 255 characters', maxLength: 255 },
|
|
125
|
+
externalRef: { type: 'string', description: "An agency's own code, up to 100 characters", maxLength: 100 },
|
|
126
|
+
regions: {
|
|
127
|
+
type: 'array',
|
|
128
|
+
items: { type: 'string', pattern: '^[A-Z]{2}$' },
|
|
129
|
+
description: "ISO 3166-1 alpha-2 codes of the countries it airs in, matched against the paying bank's country. Empty means anywhere"
|
|
130
|
+
},
|
|
131
|
+
active: {
|
|
132
|
+
type: 'boolean',
|
|
133
|
+
description: 'Whether it is in use. Governs airing and soundbite attribution; a printed code keeps resolving until the placement is deleted'
|
|
134
|
+
},
|
|
135
|
+
activeFrom: { type: 'string', description: "Start of its active window, ISO-8601 with an offset. Empty means the product's" },
|
|
136
|
+
activeTo: { type: 'string', description: "End of its active window, ISO-8601 with an offset. Empty means the product's" }
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const WEEKDAYS = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'];
|
|
140
|
+
|
|
141
|
+
const AIRING_RULE_FIELDS = {
|
|
142
|
+
frequency: { type: 'string', enum: ['ONCE', 'DAILY', 'WEEKLY', 'MONTHLY'], description: 'How the rule repeats' },
|
|
143
|
+
repeatEvery: { type: 'integer', minimum: 1, maximum: 366, description: 'Every N days, weeks or months. Default 1' },
|
|
144
|
+
startDate: { ...DATE, description: 'First day, YYYY-MM-DD, in the rule\'s time zone' },
|
|
145
|
+
endDate: { ...DATE, description: 'Last day, YYYY-MM-DD, at most 366 days after startDate. Omit for open-ended' },
|
|
146
|
+
timeZone: { type: 'string', description: "IANA time zone, e.g. Europe/Sofia. Default: the product's" },
|
|
147
|
+
daysOfWeek: { type: 'array', items: { type: 'string', enum: WEEKDAYS }, description: 'WEEKLY: the days it airs on' },
|
|
148
|
+
monthDays: {
|
|
149
|
+
type: 'array',
|
|
150
|
+
items: { type: 'integer', minimum: 1, maximum: 31 },
|
|
151
|
+
description: 'MONTHLY by date: days of the month. A day a month lacks is skipped'
|
|
152
|
+
},
|
|
153
|
+
monthWeek: { type: 'integer', enum: [1, 2, 3, 4, 5, -1], description: 'MONTHLY by weekday: which week, -1 for the last. Use with monthWeekday' },
|
|
154
|
+
monthWeekday: { type: 'string', enum: WEEKDAYS, description: 'MONTHLY by weekday: the weekday' },
|
|
155
|
+
windows: {
|
|
156
|
+
type: 'array',
|
|
157
|
+
minItems: 1,
|
|
158
|
+
maxItems: 24,
|
|
159
|
+
items: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
properties: {
|
|
163
|
+
start: { type: 'string', pattern: '^\\d{2}:\\d{2}$', description: 'HH:mm' },
|
|
164
|
+
end: { type: 'string', pattern: '^\\d{2}:\\d{2}$', description: 'HH:mm, at most 6 hours after start. At or before start runs past midnight' }
|
|
165
|
+
},
|
|
166
|
+
required: ['start', 'end']
|
|
167
|
+
},
|
|
168
|
+
description: 'Time windows on each day it airs, local to the time zone: 1 to 24, each up to 6 hours'
|
|
169
|
+
},
|
|
170
|
+
excludedDates: { type: 'array', items: { ...DATE }, description: 'Days it does not air, YYYY-MM-DD' },
|
|
171
|
+
expectedAudience: { type: 'integer', minimum: 0, description: 'Expected audience per airing, for your own planning' }
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const AIRING_RULE = {
|
|
175
|
+
type: 'object',
|
|
176
|
+
additionalProperties: false,
|
|
177
|
+
properties: AIRING_RULE_FIELDS,
|
|
178
|
+
required: ['frequency', 'startDate', 'windows']
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// --- Building blocks -----------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
const enc = encodeURIComponent;
|
|
184
|
+
|
|
185
|
+
/** The arguments named in `fields` that were given, as a request body. */
|
|
186
|
+
function pick(args, fields) {
|
|
187
|
+
return Object.fromEntries(Object.keys(fields).filter(k => args[k] !== undefined).map(k => [k, args[k]]));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function json(title, data) {
|
|
191
|
+
return [{ type: 'text', text: `**${title}**\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\`` }];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function failure(toolName, result) {
|
|
195
|
+
const e = result.error;
|
|
196
|
+
const rows = Array.isArray(e.details?.rows) && e.details.rows.length > 0
|
|
197
|
+
? `\n\n**Refused rules (nothing was saved):**\n${e.details.rows.map(r => `- #${r.index}: ${r.errorCode} - ${r.message}`).join('\n')}`
|
|
198
|
+
: '';
|
|
199
|
+
const lines = [
|
|
200
|
+
`**${toolName} failed**`,
|
|
201
|
+
'',
|
|
202
|
+
`- Status: ${e.status ?? 'no response'}`,
|
|
203
|
+
`- Error code: ${e.errorCode ?? 'N/A'}`,
|
|
204
|
+
`- Message: ${e.message ?? 'N/A'}`,
|
|
205
|
+
...(e.correlationId ? [`- Correlation ID: ${e.correlationId} (quote it to payware support)`] : []),
|
|
206
|
+
...(e.guidance ? ['', e.guidance] : [])
|
|
207
|
+
];
|
|
208
|
+
return { content: [{ type: 'text', text: lines.join('\n') + rows }], isError: true };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A placement image as MCP image content, or as text when it is SVG or not an image at all. */
|
|
212
|
+
function image(placementId, result) {
|
|
213
|
+
const type = String(result.contentType || '').split(';')[0].trim();
|
|
214
|
+
const bytes = Buffer.from(result.data);
|
|
215
|
+
if (type === 'image/png' || type === 'image/jpeg') {
|
|
216
|
+
return [
|
|
217
|
+
{ type: 'image', data: bytes.toString('base64'), mimeType: type },
|
|
218
|
+
{ type: 'text', text: `Image of placement ${placementId}: ${type}, ${bytes.length} bytes.` }
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
return [{ type: 'text', text: `**Image of placement ${placementId}** (${type || 'unknown type'}, ${bytes.length} bytes)\n\n${bytes.toString('utf8')}` }];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function define(role, spec) {
|
|
225
|
+
const r = ROLES[role];
|
|
226
|
+
return {
|
|
227
|
+
name: spec.name,
|
|
228
|
+
annotations: spec.annotations,
|
|
229
|
+
description: `${spec.description}${r.note}`,
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
additionalProperties: false,
|
|
233
|
+
properties: { ...r.properties, ...spec.properties, useSandbox: USE_SANDBOX },
|
|
234
|
+
required: [...r.required, ...(spec.required ?? [])]
|
|
235
|
+
},
|
|
236
|
+
async handler(args) {
|
|
237
|
+
const auth = r.auth(args);
|
|
238
|
+
const result = await placementsRequest({ ...spec.request(args), auth });
|
|
239
|
+
if (!result.success) {
|
|
240
|
+
return failure(spec.name, result);
|
|
241
|
+
}
|
|
242
|
+
return { content: spec.present ? spec.present(result, args) : json(spec.title, result.data) };
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --- The tools -----------------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
const PLAN_NOTE = 'Changing anything here needs the PRM plan, and below it a change answers 403 ERR_UNAUTHORIZED_OPERATION; reading works on every plan, results only on PRM. A merchant that leaves PRM keeps its placements read-only, and a printed placement code then pays as its plain product with no attribution.';
|
|
250
|
+
|
|
251
|
+
const SPECS = [
|
|
252
|
+
// Campaigns: /campaigns
|
|
253
|
+
{
|
|
254
|
+
name: 'payware_campaigns_list_campaigns',
|
|
255
|
+
annotations: READS,
|
|
256
|
+
title: 'Campaigns',
|
|
257
|
+
description: `List the merchant's campaigns. A campaign groups placements, possibly of different products, and reports their combined results.\n\n**Endpoint:** GET /campaigns\n\n${PLAN_NOTE}`,
|
|
258
|
+
properties: {},
|
|
259
|
+
request: () => ({ method: 'GET', path: '/campaigns' })
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'payware_campaigns_create_campaign',
|
|
263
|
+
annotations: CREATES,
|
|
264
|
+
title: 'Campaign created',
|
|
265
|
+
description: 'Create a campaign. Attach placements to it by giving its campaignId when you create or replace a placement.\n\n**Endpoint:** POST /campaigns (PRM plan)',
|
|
266
|
+
properties: CAMPAIGN_FIELDS,
|
|
267
|
+
required: ['name'],
|
|
268
|
+
request: (a) => ({ method: 'POST', path: '/campaigns', body: pick(a, CAMPAIGN_FIELDS) })
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'payware_campaigns_get_campaign',
|
|
272
|
+
annotations: READS,
|
|
273
|
+
title: 'Campaign',
|
|
274
|
+
description: 'Get a campaign, with the IDs of its placements you can see.\n\n**Endpoint:** GET /campaigns/{campaignId}',
|
|
275
|
+
properties: { campaignId: CAMPAIGN_ID },
|
|
276
|
+
required: ['campaignId'],
|
|
277
|
+
request: (a) => ({ method: 'GET', path: `/campaigns/${enc(a.campaignId)}` })
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'payware_campaigns_update_campaign',
|
|
281
|
+
annotations: CHANGES,
|
|
282
|
+
title: 'Campaign replaced',
|
|
283
|
+
description: '**Replace** a campaign\'s details. An omitted startsOn, endsOn or externalRef is cleared; an omitted status keeps the current one. Get the campaign first and send every value you want to keep.\n\n**Endpoint:** PUT /campaigns/{campaignId} (PRM plan)',
|
|
284
|
+
properties: { campaignId: CAMPAIGN_ID, ...CAMPAIGN_FIELDS },
|
|
285
|
+
required: ['campaignId', 'name'],
|
|
286
|
+
request: (a) => ({ method: 'PUT', path: `/campaigns/${enc(a.campaignId)}`, body: pick(a, CAMPAIGN_FIELDS) })
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'payware_campaigns_delete_campaign',
|
|
290
|
+
annotations: CHANGES,
|
|
291
|
+
title: 'Campaign deleted',
|
|
292
|
+
description: 'Delete a campaign. Its placements stay, detached from it, and keep working.\n\n**Endpoint:** DELETE /campaigns/{campaignId} (PRM plan)',
|
|
293
|
+
properties: { campaignId: CAMPAIGN_ID },
|
|
294
|
+
required: ['campaignId'],
|
|
295
|
+
request: (a) => ({ method: 'DELETE', path: `/campaigns/${enc(a.campaignId)}` }),
|
|
296
|
+
present: (r, a) => [{ type: 'text', text: `Campaign ${a.campaignId} deleted. Its placements are detached and keep working.` }]
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'payware_campaigns_get_campaign_stats',
|
|
300
|
+
annotations: READS,
|
|
301
|
+
title: 'Campaign results',
|
|
302
|
+
description: 'A campaign\'s results over [from, to): offers shown when a bank resolved one of its placement IDs (resolutions), offers shown after a recognised soundbite tap (recognitions), payments started (transactions) and confirmed, and confirmed amounts by currency.\n\n**Endpoint:** GET /campaigns/{campaignId}/stats (PRM plan)',
|
|
303
|
+
properties: { campaignId: CAMPAIGN_ID, ...STATS_RANGE },
|
|
304
|
+
required: ['campaignId'],
|
|
305
|
+
request: (a) => ({ method: 'GET', path: `/campaigns/${enc(a.campaignId)}/stats`, query: { from: a.from, to: a.to } })
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// Placements: /products/{productId}/placements
|
|
309
|
+
{
|
|
310
|
+
name: 'payware_products_list_placements',
|
|
311
|
+
annotations: READS,
|
|
312
|
+
title: 'Placements',
|
|
313
|
+
description: `List a product's placements. A placement is one way the product is distributed - a poster, a TV spot, a packaging label - with its own ID (pl + 8 characters), so every payment it starts is attributed to it.\n\n**Endpoint:** GET /products/{productId}/placements\n\n${PLAN_NOTE}`,
|
|
314
|
+
properties: { productId: PRODUCT_ID },
|
|
315
|
+
required: ['productId'],
|
|
316
|
+
request: (a) => ({ method: 'GET', path: `/products/${enc(a.productId)}/placements` })
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'payware_products_create_placement',
|
|
320
|
+
annotations: CREATES,
|
|
321
|
+
title: 'Placement created',
|
|
322
|
+
description: 'Create a placement for a product; up to 1,000 per product. The response carries its placementId, which goes on the medium wherever the product ID would: a QR code (payware://{placementId}), an NFC tag, a BLE beacon, a link, a barcode or an SMS. A bank resolving it pays for the product, and the payment is attributed to the placement. For a SOUNDBITE carrier, add airing rules so taps during the airing are attributed.\n\n**Endpoint:** POST /products/{productId}/placements (PRM plan)',
|
|
323
|
+
properties: { productId: PRODUCT_ID, ...PLACEMENT_FIELDS },
|
|
324
|
+
required: ['productId', 'name', 'medium', 'carriers'],
|
|
325
|
+
request: (a) => ({ method: 'POST', path: `/products/${enc(a.productId)}/placements`, body: pick(a, PLACEMENT_FIELDS) })
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: 'payware_products_get_placement',
|
|
329
|
+
annotations: READS,
|
|
330
|
+
title: 'Placement',
|
|
331
|
+
description: 'Get a placement.\n\n**Endpoint:** GET /products/{productId}/placements/{placementId}',
|
|
332
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID },
|
|
333
|
+
required: ['productId', 'placementId'],
|
|
334
|
+
request: (a) => ({ method: 'GET', path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}` })
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'payware_products_update_placement',
|
|
338
|
+
annotations: CHANGES,
|
|
339
|
+
title: 'Placement replaced',
|
|
340
|
+
description: '**Replace** a placement\'s details. Every field is replaced, so an omitted optional field is cleared - an omitted campaignId detaches it from its campaign; an omitted active keeps the current value. Get the placement first and send every value you want to keep. The placement ID does not change, so printed codes keep working.\n\n**Endpoint:** PUT /products/{productId}/placements/{placementId} (PRM plan)',
|
|
341
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID, ...PLACEMENT_FIELDS },
|
|
342
|
+
required: ['productId', 'placementId', 'name', 'medium', 'carriers'],
|
|
343
|
+
request: (a) => ({
|
|
344
|
+
method: 'PUT',
|
|
345
|
+
path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}`,
|
|
346
|
+
body: pick(a, PLACEMENT_FIELDS)
|
|
347
|
+
})
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: 'payware_products_delete_placement',
|
|
351
|
+
annotations: CHANGES,
|
|
352
|
+
title: 'Placement deleted',
|
|
353
|
+
description: 'Delete a placement. **Its printed codes stop resolving**, its short link is retired and its ID is never reused. To stop airing and attribution but keep printed codes working, replace it with active false instead.\n\n**Endpoint:** DELETE /products/{productId}/placements/{placementId} (PRM plan)',
|
|
354
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID },
|
|
355
|
+
required: ['productId', 'placementId'],
|
|
356
|
+
request: (a) => ({ method: 'DELETE', path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}` }),
|
|
357
|
+
present: (r, a) => [{ type: 'text', text: `Placement ${a.placementId} deleted. Its codes no longer resolve and its short link is retired.` }]
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: 'payware_products_get_placement_image',
|
|
361
|
+
annotations: READS,
|
|
362
|
+
title: 'Placement image',
|
|
363
|
+
description: 'A placement\'s QR code (carrying payware://{placementId}) or Code 128 barcode (carrying the plain ID), for print. PNG and JPG come back as an image, SVG as its markup.\n\n**Endpoint:** GET /products/{productId}/placements/{placementId}/image',
|
|
364
|
+
properties: {
|
|
365
|
+
productId: PRODUCT_ID,
|
|
366
|
+
placementId: PLACEMENT_ID,
|
|
367
|
+
type: { type: 'string', enum: ['QR', 'BARCODE'], description: 'QR code or barcode. Default QR', default: 'QR' },
|
|
368
|
+
format: { type: 'string', enum: ['PNG', 'JPG', 'SVG'], description: 'Image format. Default PNG', default: 'PNG' }
|
|
369
|
+
},
|
|
370
|
+
required: ['productId', 'placementId'],
|
|
371
|
+
request: (a) => ({
|
|
372
|
+
method: 'GET',
|
|
373
|
+
path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/image`,
|
|
374
|
+
query: { type: a.type ?? 'QR', format: a.format ?? 'PNG' },
|
|
375
|
+
binary: true
|
|
376
|
+
}),
|
|
377
|
+
present: (r, a) => image(a.placementId, r)
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: 'payware_products_get_placement_link',
|
|
381
|
+
annotations: ADDS,
|
|
382
|
+
title: 'Placement link',
|
|
383
|
+
description: 'A placement\'s short link, https://go.payware.eu/{placementId}, made on the first request and the same afterwards. On a phone it opens payware:/{placementId} in the payment app; a payment through it is attributed to the placement.\n\n**Endpoint:** GET /products/{productId}/placements/{placementId}/link (PRM plan)',
|
|
384
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID },
|
|
385
|
+
required: ['productId', 'placementId'],
|
|
386
|
+
request: (a) => ({ method: 'GET', path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/link` }),
|
|
387
|
+
present: (r, a) => [{
|
|
388
|
+
type: 'text',
|
|
389
|
+
text: `Short link of placement ${a.placementId}:\n\n${typeof r.data === 'string' ? r.data : JSON.stringify(r.data)}`
|
|
390
|
+
}]
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: 'payware_products_get_placement_stats',
|
|
394
|
+
annotations: READS,
|
|
395
|
+
title: 'Placement results',
|
|
396
|
+
description: 'A placement\'s results over [from, to): offers shown when a bank resolved its ID (resolutions), offers shown after a recognised soundbite tap (recognitions), payments started (transactions) and confirmed, and confirmed amounts by currency.\n\n**Endpoint:** GET /products/{productId}/placements/{placementId}/stats (PRM plan)',
|
|
397
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID, ...STATS_RANGE },
|
|
398
|
+
required: ['productId', 'placementId'],
|
|
399
|
+
request: (a) => ({
|
|
400
|
+
method: 'GET',
|
|
401
|
+
path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/stats`,
|
|
402
|
+
query: { from: a.from, to: a.to }
|
|
403
|
+
})
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
// Airings: /products/{productId}/placements/{placementId}/airings, and /placements/calendar
|
|
407
|
+
{
|
|
408
|
+
name: 'payware_products_list_airings',
|
|
409
|
+
annotations: READS,
|
|
410
|
+
title: 'Airing rules',
|
|
411
|
+
description: 'List a placement\'s airing rules. A rule is a recurrence with time windows, in an IANA time zone; a soundbite tap during an airing is attributed to the placement.\n\n**Endpoint:** GET /products/{productId}/placements/{placementId}/airings',
|
|
412
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID },
|
|
413
|
+
required: ['productId', 'placementId'],
|
|
414
|
+
request: (a) => ({ method: 'GET', path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/airings` })
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'payware_products_create_airing',
|
|
418
|
+
annotations: CREATES,
|
|
419
|
+
title: 'Airing rule added',
|
|
420
|
+
description: 'Add an airing rule to a placement: up to 100 rules per placement. Example: "daily 08:00-08:30 and 15:40-16:10" is one DAILY rule with two windows. Check `warnings` in the response: AIRING_OVERLAP means another soundbite placement of the same product airs at the same time in an overlapping region, and a tap then is attributed to neither.\n\n**Endpoint:** POST /products/{productId}/placements/{placementId}/airings (PRM plan)',
|
|
421
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID, ...AIRING_RULE_FIELDS },
|
|
422
|
+
required: ['productId', 'placementId', 'frequency', 'startDate', 'windows'],
|
|
423
|
+
request: (a) => ({
|
|
424
|
+
method: 'POST',
|
|
425
|
+
path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/airings`,
|
|
426
|
+
body: pick(a, AIRING_RULE_FIELDS)
|
|
427
|
+
})
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: 'payware_products_replace_airings',
|
|
431
|
+
annotations: CHANGES,
|
|
432
|
+
title: 'Airing rules replaced',
|
|
433
|
+
description: '**Replace all** of a placement\'s airing rules with this list, all or nothing: when any rule is refused, nothing is saved and each refused rule is listed by its index. An empty list removes every rule. Suited to loading an agency\'s schedule in one call.\n\n**Endpoint:** PUT /products/{productId}/placements/{placementId}/airings (PRM plan)',
|
|
434
|
+
properties: {
|
|
435
|
+
productId: PRODUCT_ID,
|
|
436
|
+
placementId: PLACEMENT_ID,
|
|
437
|
+
rules: { type: 'array', maxItems: 100, items: AIRING_RULE, description: 'The complete new set of airing rules, up to 100' }
|
|
438
|
+
},
|
|
439
|
+
required: ['productId', 'placementId', 'rules'],
|
|
440
|
+
request: (a) => ({
|
|
441
|
+
method: 'PUT',
|
|
442
|
+
path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/airings`,
|
|
443
|
+
body: a.rules
|
|
444
|
+
})
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'payware_products_get_next_airings',
|
|
448
|
+
annotations: READS,
|
|
449
|
+
title: 'Next airings',
|
|
450
|
+
description: 'A placement\'s next airings, each in its rule\'s time zone so daylight-saving shifts are visible. Use it to check a rule does what was meant before relying on it.\n\n**Endpoint:** GET /products/{productId}/placements/{placementId}/airings/next',
|
|
451
|
+
properties: {
|
|
452
|
+
productId: PRODUCT_ID,
|
|
453
|
+
placementId: PLACEMENT_ID,
|
|
454
|
+
count: { type: 'integer', minimum: 1, maximum: 100, description: 'How many, 1 to 100. Default 10' }
|
|
455
|
+
},
|
|
456
|
+
required: ['productId', 'placementId'],
|
|
457
|
+
request: (a) => ({
|
|
458
|
+
method: 'GET',
|
|
459
|
+
path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/airings/next`,
|
|
460
|
+
query: { count: a.count }
|
|
461
|
+
})
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: 'payware_products_get_airing',
|
|
465
|
+
annotations: READS,
|
|
466
|
+
title: 'Airing rule',
|
|
467
|
+
description: 'Get one airing rule.\n\n**Endpoint:** GET /products/{productId}/placements/{placementId}/airings/{ruleId}',
|
|
468
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID, ruleId: RULE_ID },
|
|
469
|
+
required: ['productId', 'placementId', 'ruleId'],
|
|
470
|
+
request: (a) => ({ method: 'GET', path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/airings/${enc(a.ruleId)}` })
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'payware_products_update_airing',
|
|
474
|
+
annotations: CHANGES,
|
|
475
|
+
title: 'Airing rule replaced',
|
|
476
|
+
description: '**Replace** one airing rule: every field is replaced, so send the whole rule. Check `warnings` in the response for AIRING_OVERLAP.\n\n**Endpoint:** PUT /products/{productId}/placements/{placementId}/airings/{ruleId} (PRM plan)',
|
|
477
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID, ruleId: RULE_ID, ...AIRING_RULE_FIELDS },
|
|
478
|
+
required: ['productId', 'placementId', 'ruleId', 'frequency', 'startDate', 'windows'],
|
|
479
|
+
request: (a) => ({
|
|
480
|
+
method: 'PUT',
|
|
481
|
+
path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/airings/${enc(a.ruleId)}`,
|
|
482
|
+
body: pick(a, AIRING_RULE_FIELDS)
|
|
483
|
+
})
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: 'payware_products_delete_airing',
|
|
487
|
+
annotations: CHANGES,
|
|
488
|
+
title: 'Airing rule deleted',
|
|
489
|
+
description: 'Delete one airing rule.\n\n**Endpoint:** DELETE /products/{productId}/placements/{placementId}/airings/{ruleId} (PRM plan)',
|
|
490
|
+
properties: { productId: PRODUCT_ID, placementId: PLACEMENT_ID, ruleId: RULE_ID },
|
|
491
|
+
required: ['productId', 'placementId', 'ruleId'],
|
|
492
|
+
request: (a) => ({ method: 'DELETE', path: `/products/${enc(a.productId)}/placements/${enc(a.placementId)}/airings/${enc(a.ruleId)}` }),
|
|
493
|
+
present: (r, a) => [{ type: 'text', text: `Airing rule ${a.ruleId} of placement ${a.placementId} deleted.` }]
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'payware_products_get_airing_calendar',
|
|
497
|
+
annotations: READS,
|
|
498
|
+
title: 'Airing calendar',
|
|
499
|
+
description: 'Every airing of the merchant\'s placements between from and to, across all products, ordered by start time and each in its rule\'s time zone. Answers "what is on air this week" in one call. The range may span at most 400 days.\n\n**Endpoint:** GET /placements/calendar',
|
|
500
|
+
properties: {
|
|
501
|
+
from: { type: 'string', description: `Start of the range: ${RANGE_FORMS}` },
|
|
502
|
+
to: { type: 'string', description: 'End of the range, after from, in the same forms' }
|
|
503
|
+
},
|
|
504
|
+
required: ['from', 'to'],
|
|
505
|
+
request: (a) => ({ method: 'GET', path: '/placements/calendar', query: { from: a.from, to: a.to } })
|
|
506
|
+
}
|
|
507
|
+
];
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* The campaign, placement and airing tools for a role.
|
|
511
|
+
* @param {'merchant'|'isv'} role
|
|
512
|
+
* @returns {Array} tool definitions
|
|
513
|
+
*/
|
|
514
|
+
export function placementTools(role) {
|
|
515
|
+
if (!ROLES[role]) {
|
|
516
|
+
throw new Error(`No placement tools for role ${role}`);
|
|
517
|
+
}
|
|
518
|
+
return SPECS.map(spec => define(role, spec));
|
|
519
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createAuthHeaders } from '../auth-headers.js';
|
|
|
3
3
|
import { apiErrorResult } from '../api-errors.js';
|
|
4
4
|
import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
|
|
5
5
|
import { getSandboxUrl, getProductionUrl } from '../../config/env.js';
|
|
6
|
+
import { CREATES } from '../tool-annotations.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Creating POIs (Points of Interaction) - the physical payment points a payer scans.
|
|
@@ -206,6 +207,7 @@ export function buildCreatePOITool({ partnerType, resolveAuth }) {
|
|
|
206
207
|
|
|
207
208
|
return {
|
|
208
209
|
name: 'payware_poi_create',
|
|
210
|
+
annotations: CREATES,
|
|
209
211
|
description: `Create a single POI (Point of Interaction) - a physical payment point a payer scans.
|
|
210
212
|
|
|
211
213
|
**Endpoint:** POST /poi -> 201 Created
|
|
@@ -296,6 +298,7 @@ export function buildCreatePOIBatchTool({ partnerType, resolveAuth }) {
|
|
|
296
298
|
|
|
297
299
|
return {
|
|
298
300
|
name: 'payware_poi_create_batch',
|
|
301
|
+
annotations: CREATES,
|
|
299
302
|
description: `Create up to ${MAX_POI_BATCH_SIZE} POIs in a single **atomic** call - the tool for rolling out a terminal estate.
|
|
300
303
|
|
|
301
304
|
**Endpoint:** POST /poi/batch -> 201 Created, returning \`{ created, pois: [...] }\`
|
|
@@ -802,35 +802,46 @@ export async function updateAudio({
|
|
|
802
802
|
throw new Error('audioId is required');
|
|
803
803
|
}
|
|
804
804
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
805
|
+
// PATCH is metadata only. The server refuses a different productId there with
|
|
806
|
+
// ERR_AUDIO_REASSIGN_FORBIDDEN and moves an audio through POST .../{id}/transfer instead
|
|
807
|
+
// (AudioService.update), so a rename and a move are two calls. The rename goes first: if the move
|
|
808
|
+
// is then refused, the title the caller asked for is still applied.
|
|
809
|
+
const move = newProductId !== undefined && newProductId !== productId;
|
|
810
|
+
if (title === undefined && !move) {
|
|
811
|
+
throw new Error('Nothing to update: provide a title, or a newProductId different from productId');
|
|
812
|
+
}
|
|
809
813
|
|
|
810
|
-
const minimizedBody = createMinimizedJSON(requestBody);
|
|
811
|
-
const headers = await createAuthHeaders(partnerType, { ...authParams, requestBody: minimizedBody });
|
|
812
814
|
const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
|
|
815
|
+
const audioUrl = `${baseUrl}/products/${productId}/audios/${audioId}`;
|
|
816
|
+
|
|
817
|
+
const send = async (method, url, requestBody, operation) => {
|
|
818
|
+
const minimizedBody = createMinimizedJSON(requestBody);
|
|
819
|
+
const headers = await createAuthHeaders(partnerType, { ...authParams, requestBody: minimizedBody });
|
|
820
|
+
try {
|
|
821
|
+
const response = await axios[method](url, minimizedBody, { headers });
|
|
822
|
+
return {
|
|
823
|
+
success: true,
|
|
824
|
+
data: response.data,
|
|
825
|
+
requestInfo: {
|
|
826
|
+
url,
|
|
827
|
+
method: method.toUpperCase(),
|
|
828
|
+
requestBody: minimizedBody,
|
|
829
|
+
statusCode: response.status
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
} catch (error) {
|
|
833
|
+
throw describeApiError(error, operation);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
813
836
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
);
|
|
820
|
-
|
|
821
|
-
return {
|
|
822
|
-
success: true,
|
|
823
|
-
data: response.data,
|
|
824
|
-
requestInfo: {
|
|
825
|
-
url: `${baseUrl}/products/${productId}/audios/${audioId}`,
|
|
826
|
-
method: 'PATCH',
|
|
827
|
-
requestBody: minimizedBody,
|
|
828
|
-
statusCode: response.status
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
} catch (error) {
|
|
832
|
-
throw describeApiError(error, 'update audio');
|
|
837
|
+
let result;
|
|
838
|
+
if (title !== undefined) {
|
|
839
|
+
result = await send('patch', audioUrl, { title }, 'update audio');
|
|
840
|
+
}
|
|
841
|
+
if (move) {
|
|
842
|
+
result = await send('post', `${audioUrl}/transfer`, { targetProductId: newProductId }, 'transfer audio');
|
|
833
843
|
}
|
|
844
|
+
return result;
|
|
834
845
|
}
|
|
835
846
|
|
|
836
847
|
/**
|
|
@@ -2,6 +2,7 @@ import axios from 'axios';
|
|
|
2
2
|
import { createAuthHeaders } from '../auth-headers.js';
|
|
3
3
|
import { apiErrorResult } from '../api-errors.js';
|
|
4
4
|
import { getSandboxUrl, getProductionUrl } from '../../config/env.js';
|
|
5
|
+
import { READS } from '../tool-annotations.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Shops - the merchant outlets a transaction, product or POI belongs to.
|
|
@@ -48,6 +49,7 @@ export function buildListShopsTool({ partnerType, resolveAuth }) {
|
|
|
48
49
|
|
|
49
50
|
return {
|
|
50
51
|
name: 'payware_shops_list',
|
|
52
|
+
annotations: READS,
|
|
51
53
|
description: `List the merchant outlets (shops) the caller can act on.
|
|
52
54
|
|
|
53
55
|
**Endpoint:** GET /shops
|