@jsm-mit/sultana-agent-tools-package 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/dist/create-salon-tools.d.ts +37 -0
- package/dist/create-salon-tools.d.ts.map +1 -0
- package/dist/create-salon-tools.js +30 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/ic-salon-core-port.d.ts +57 -0
- package/dist/ic-salon-core-port.d.ts.map +1 -0
- package/dist/ic-salon-core-port.js +160 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/persona.d.ts +18 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +49 -0
- package/dist/salon-core-port.d.ts +144 -0
- package/dist/salon-core-port.d.ts.map +1 -0
- package/dist/salon-core-port.js +11 -0
- package/dist/tools/args.d.ts +30 -0
- package/dist/tools/args.d.ts.map +1 -0
- package/dist/tools/args.js +89 -0
- package/dist/tools/promos.d.ts +7 -0
- package/dist/tools/promos.d.ts.map +1 -0
- package/dist/tools/promos.js +438 -0
- package/dist/tools/schedule.d.ts +4 -0
- package/dist/tools/schedule.d.ts.map +1 -0
- package/dist/tools/schedule.js +300 -0
- package/dist/tools/services.d.ts +7 -0
- package/dist/tools/services.d.ts.map +1 -0
- package/dist/tools/services.js +309 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/package.json +48 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { MAX_ACTIVE_PROMOS_PER_SALON, MAX_PROMOS_PER_SALON } from "@jsm-mit/sultana-core-motoko-package";
|
|
2
|
+
import { toToolError } from "../errors.js";
|
|
3
|
+
import { err, needsConfirmation, ok } from "../types.js";
|
|
4
|
+
import { ArgumentError, phrasesMatch, readBoolean, readOptionalNumber, readOptionalString, readOptionalStringArray, readString, } from "./args.js";
|
|
5
|
+
const MAX_PROMO_NAME_LENGTH = 60;
|
|
6
|
+
const MAX_TEXT_LINES = 2;
|
|
7
|
+
const MAX_CHIPS = 2;
|
|
8
|
+
const CONFIRMED_FIELD = {
|
|
9
|
+
type: "boolean",
|
|
10
|
+
description: "false dla podglądu: narzędzie nic nie zapisze i odda opis zmiany do zatwierdzenia przez właściciela. true dopiero po potwierdzeniu.",
|
|
11
|
+
};
|
|
12
|
+
export function createPromoTools(port) {
|
|
13
|
+
return [
|
|
14
|
+
listMediaTool(port),
|
|
15
|
+
listPromosTool(port),
|
|
16
|
+
addPromoTool(port),
|
|
17
|
+
updatePromoTool(port),
|
|
18
|
+
setPromoActiveTool(port),
|
|
19
|
+
removePromoTool(port),
|
|
20
|
+
listDiscountCodesTool(port),
|
|
21
|
+
addDiscountCodeTool(port),
|
|
22
|
+
removeDiscountCodeTool(port),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
function listMediaTool(port) {
|
|
26
|
+
return {
|
|
27
|
+
name: "list_media",
|
|
28
|
+
progress: "Sprawdzam bibliotekę materiałów…",
|
|
29
|
+
description: "Zwraca gotowe materiały z biblioteki salonu (zdjęcia i filmy) wraz z ich assetId. Promocję da się założyć wyłącznie na takim materiale — bez niego trzeba poprosić właściciela o wgranie pliku w panelu.",
|
|
30
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
31
|
+
execute: async () => {
|
|
32
|
+
try {
|
|
33
|
+
const media = (await port.listMedia()).filter((item) => item.kind !== "audio");
|
|
34
|
+
if (media.length === 0) {
|
|
35
|
+
return ok("Biblioteka salonu jest pusta — nie ma na czym założyć promocji. Poproś właściciela o wgranie zdjęcia lub filmu w panelu.", []);
|
|
36
|
+
}
|
|
37
|
+
return ok(`W bibliotece jest ${media.length} materiał(ów).`, media);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return toToolError(error, "Nie udało się odczytać biblioteki materiałów.");
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** As with services: a public read holds no drafts, so its texts must not speak of a library the
|
|
46
|
+
* model cannot see. */
|
|
47
|
+
const LIST_PROMOS_TEXT = {
|
|
48
|
+
owner: {
|
|
49
|
+
description: "Zwraca promocje salonu: id, nazwę, czy jest aktywna, materiał, teksty i kod rabatowy.",
|
|
50
|
+
empty: "Salon nie ma jeszcze żadnej promocji.",
|
|
51
|
+
count: (promos) => {
|
|
52
|
+
const active = promos.filter((promo) => promo.active).length;
|
|
53
|
+
return `Salon ma ${promos.length} promocji (limit ${MAX_PROMOS_PER_SALON}), aktywnych ${active} z ${MAX_ACTIVE_PROMOS_PER_SALON}.`;
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
public: {
|
|
57
|
+
description: "Zwraca AKTYWNE promocje salonu — te, które widać w kanale: id, nazwę, materiał, teksty i kod rabatowy. Wyłączonych promocji tu nie ma.",
|
|
58
|
+
empty: "Salon nie ma żadnej aktywnej promocji.",
|
|
59
|
+
count: (promos) => `Aktywne promocje salonu: ${promos.length}.`,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
/** `access` must match the port: `"public"` only for a port read anonymously, because the canister
|
|
63
|
+
* gives the salon's owner the whole library whatever the port calls. */
|
|
64
|
+
export function listPromosTool(port, access = "owner") {
|
|
65
|
+
const text = LIST_PROMOS_TEXT[access];
|
|
66
|
+
return {
|
|
67
|
+
name: "list_promos",
|
|
68
|
+
progress: "Sprawdzam promocje…",
|
|
69
|
+
description: text.description,
|
|
70
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
71
|
+
execute: async () => {
|
|
72
|
+
try {
|
|
73
|
+
const promos = await port.listPromos();
|
|
74
|
+
if (promos.length === 0)
|
|
75
|
+
return ok(text.empty, []);
|
|
76
|
+
return ok(text.count(promos), promos);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return toToolError(error, "Nie udało się odczytać promocji.");
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function addPromoTool(port) {
|
|
85
|
+
return {
|
|
86
|
+
name: "add_promo",
|
|
87
|
+
progress: "Zakładam promocję…",
|
|
88
|
+
description: "Zakłada nową promocję na materiale z biblioteki. Najpierw wywołaj list_media i wybierz assetId. Nowa promocja jest nieaktywna — włącz ją potem przez set_promo_active.",
|
|
89
|
+
parameters: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
name: { type: "string", description: `Nazwa kampanii, maksymalnie ${MAX_PROMO_NAME_LENGTH} znaków.` },
|
|
93
|
+
assetId: { type: "string", description: "Id materiału z list_media (zdjęcie albo film)." },
|
|
94
|
+
textLines: {
|
|
95
|
+
type: "array",
|
|
96
|
+
items: { type: "string" },
|
|
97
|
+
description: `Do ${MAX_TEXT_LINES} linii tekstu na materiale.`,
|
|
98
|
+
},
|
|
99
|
+
chips: { type: "array", items: { type: "string" }, description: `Do ${MAX_CHIPS} krótkich etykiet.` },
|
|
100
|
+
buttonLabel: { type: "string", description: "Napis na przycisku. Pominięty — domyślny napis aplikacji." },
|
|
101
|
+
discountCode: { type: "string", description: "Kod rabatowy salonu do rozgłoszenia. Musi już istnieć." },
|
|
102
|
+
targetServiceId: { type: "string", description: "Usługa, do której prowadzi kliknięcie. Pominięta — cały salon." },
|
|
103
|
+
confirmed: CONFIRMED_FIELD,
|
|
104
|
+
},
|
|
105
|
+
required: ["name", "assetId", "confirmed"],
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
},
|
|
108
|
+
execute: async (args) => {
|
|
109
|
+
try {
|
|
110
|
+
const input = await buildPromoInput(port, args, null);
|
|
111
|
+
if (!readBoolean(args, "confirmed")) {
|
|
112
|
+
return needsConfirmation(`Założę promocję ${await describePromo(port, input)}.`, { ...args, confirmed: true });
|
|
113
|
+
}
|
|
114
|
+
const promoId = await port.addPromo(input);
|
|
115
|
+
return ok(`Założono promocję „${input.name}”. Jest jeszcze nieaktywna.`, { promoId });
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof ArgumentError)
|
|
119
|
+
return err("invalid_arguments", error.message);
|
|
120
|
+
return toToolError(error, "Nie udało się założyć promocji.");
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function updatePromoTool(port) {
|
|
126
|
+
return {
|
|
127
|
+
name: "update_promo",
|
|
128
|
+
progress: "Zmieniam promocję…",
|
|
129
|
+
description: "Zmienia istniejącą promocję. Podaj tylko pola, które mają się zmienić — reszta zostanie zachowana. Promocja zachowuje swoje miejsce w kanale, więc poprawka nie restartuje kampanii.",
|
|
130
|
+
parameters: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
promoId: { type: "string", description: "Id promocji z list_promos." },
|
|
134
|
+
name: { type: "string", description: "Nowa nazwa kampanii." },
|
|
135
|
+
assetId: { type: "string", description: "Nowy materiał z list_media." },
|
|
136
|
+
textLines: { type: "array", items: { type: "string" }, description: "Nowe linie tekstu." },
|
|
137
|
+
chips: { type: "array", items: { type: "string" }, description: "Nowe etykiety." },
|
|
138
|
+
buttonLabel: { type: "string", description: "Nowy napis na przycisku." },
|
|
139
|
+
discountCode: { type: "string", description: "Nowy kod rabatowy." },
|
|
140
|
+
targetServiceId: { type: "string", description: "Nowy cel kliknięcia." },
|
|
141
|
+
confirmed: CONFIRMED_FIELD,
|
|
142
|
+
},
|
|
143
|
+
required: ["promoId", "confirmed"],
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
},
|
|
146
|
+
execute: async (args) => {
|
|
147
|
+
try {
|
|
148
|
+
const promoId = readString(args, "promoId");
|
|
149
|
+
const current = await findPromo(port, promoId);
|
|
150
|
+
if (!current)
|
|
151
|
+
return err("not_found", `W tym salonie nie ma promocji o id ${promoId}.`);
|
|
152
|
+
const input = await buildPromoInput(port, args, current);
|
|
153
|
+
if (!readBoolean(args, "confirmed")) {
|
|
154
|
+
return needsConfirmation(`Zmienię promocję „${current.name}” na: ${await describePromo(port, input)}.`, {
|
|
155
|
+
...args,
|
|
156
|
+
confirmed: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
await port.updatePromo(promoId, input);
|
|
160
|
+
return ok(`Zmieniono promocję „${input.name}”.`);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof ArgumentError)
|
|
164
|
+
return err("invalid_arguments", error.message);
|
|
165
|
+
return toToolError(error, "Nie udało się zmienić promocji.");
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function setPromoActiveTool(port) {
|
|
171
|
+
return {
|
|
172
|
+
name: "set_promo_active",
|
|
173
|
+
progress: "Zmieniam status promocji…",
|
|
174
|
+
description: `Włącza promocję w kanale albo ją wyłącza. Naraz może być aktywnych najwyżej ${MAX_ACTIVE_PROMOS_PER_SALON} — żeby włączyć kolejną, najpierw wyłącz inną.`,
|
|
175
|
+
parameters: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
promoId: { type: "string", description: "Id promocji z list_promos." },
|
|
179
|
+
active: { type: "boolean", description: "true włącza, false wyłącza." },
|
|
180
|
+
confirmed: CONFIRMED_FIELD,
|
|
181
|
+
},
|
|
182
|
+
required: ["promoId", "active", "confirmed"],
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
},
|
|
185
|
+
execute: async (args) => {
|
|
186
|
+
try {
|
|
187
|
+
const promoId = readString(args, "promoId");
|
|
188
|
+
const current = await findPromo(port, promoId);
|
|
189
|
+
if (!current)
|
|
190
|
+
return err("not_found", `W tym salonie nie ma promocji o id ${promoId}.`);
|
|
191
|
+
const active = readBoolean(args, "active");
|
|
192
|
+
const verb = active ? "włączę" : "wyłączę";
|
|
193
|
+
if (!readBoolean(args, "confirmed")) {
|
|
194
|
+
return needsConfirmation(`${verb[0].toUpperCase()}${verb.slice(1)} promocję „${current.name}”.`, {
|
|
195
|
+
...args,
|
|
196
|
+
confirmed: true,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
await port.setPromoActive(promoId, active);
|
|
200
|
+
return ok(`Promocja „${current.name}” jest teraz ${active ? "aktywna" : "wyłączona"}.`);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error instanceof ArgumentError)
|
|
204
|
+
return err("invalid_arguments", error.message);
|
|
205
|
+
return toToolError(error, "Nie udało się zmienić statusu promocji.");
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function removePromoTool(port) {
|
|
211
|
+
return {
|
|
212
|
+
name: "remove_promo",
|
|
213
|
+
progress: "Kasuję promocję…",
|
|
214
|
+
description: "Kasuje promocję z biblioteki. Nieodwracalne — właściciel musi przepisać jej nazwę w confirmationPhrase. Żeby tylko zdjąć ją z kanału, użyj set_promo_active.",
|
|
215
|
+
parameters: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
promoId: { type: "string", description: "Id promocji z list_promos." },
|
|
219
|
+
confirmationPhrase: { type: "string", description: "Dokładna nazwa kasowanej promocji." },
|
|
220
|
+
confirmed: CONFIRMED_FIELD,
|
|
221
|
+
},
|
|
222
|
+
required: ["promoId", "confirmed"],
|
|
223
|
+
additionalProperties: false,
|
|
224
|
+
},
|
|
225
|
+
execute: async (args) => {
|
|
226
|
+
try {
|
|
227
|
+
const promoId = readString(args, "promoId");
|
|
228
|
+
const current = await findPromo(port, promoId);
|
|
229
|
+
if (!current)
|
|
230
|
+
return err("not_found", `W tym salonie nie ma promocji o id ${promoId}.`);
|
|
231
|
+
if (!readBoolean(args, "confirmed")) {
|
|
232
|
+
return needsConfirmation(`Skasuję promocję „${current.name}”. Tego nie da się cofnąć — poproś właściciela o przepisanie nazwy. Jeśli chodzi tylko o zdjęcie z kanału, lepiej ją wyłączyć.`, { promoId, confirmationPhrase: current.name, confirmed: true });
|
|
233
|
+
}
|
|
234
|
+
const phrase = readOptionalString(args, "confirmationPhrase");
|
|
235
|
+
if (!phrase || !phrasesMatch(phrase, current.name)) {
|
|
236
|
+
return err("invalid_arguments", `Aby skasować promocję, właściciel musi przepisać jej nazwę: „${current.name}”.`);
|
|
237
|
+
}
|
|
238
|
+
await port.removePromo(promoId);
|
|
239
|
+
return ok(`Skasowano promocję „${current.name}”.`);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error instanceof ArgumentError)
|
|
243
|
+
return err("invalid_arguments", error.message);
|
|
244
|
+
return toToolError(error, "Nie udało się skasować promocji.");
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function listDiscountCodesTool(port) {
|
|
250
|
+
return {
|
|
251
|
+
name: "list_discount_codes",
|
|
252
|
+
progress: "Sprawdzam kody rabatowe…",
|
|
253
|
+
description: "Zwraca kody rabatowe salonu: treść kodu, wartość i zasięg.",
|
|
254
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
255
|
+
execute: async () => {
|
|
256
|
+
try {
|
|
257
|
+
const codes = await port.listDiscountCodes();
|
|
258
|
+
if (codes.length === 0)
|
|
259
|
+
return ok("Salon nie ma jeszcze żadnego kodu rabatowego.", []);
|
|
260
|
+
return ok(`Salon ma ${codes.length} kod(ów) rabatowych.`, codes);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
return toToolError(error, "Nie udało się odczytać kodów rabatowych.");
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function addDiscountCodeTool(port) {
|
|
269
|
+
return {
|
|
270
|
+
name: "add_discount_code",
|
|
271
|
+
progress: "Dodaję kod rabatowy…",
|
|
272
|
+
description: "Dodaje kod rabatowy salonu. Podaj albo percent, albo amountPln — nie oba. Kod nie ma daty ważności ani limitu użyć; działa, dopóki go nie usuniesz.",
|
|
273
|
+
parameters: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
code: { type: "string", description: "Treść kodu, którą wpisze klientka." },
|
|
277
|
+
percent: { type: "integer", description: "Rabat procentowy, 1-100." },
|
|
278
|
+
amountPln: { type: "integer", description: "Rabat kwotowy w pełnych złotych." },
|
|
279
|
+
scopeServiceId: { type: "string", description: "Ogranicz kod do jednej usługi. Pominięty — cały salon." },
|
|
280
|
+
confirmed: CONFIRMED_FIELD,
|
|
281
|
+
},
|
|
282
|
+
required: ["code", "confirmed"],
|
|
283
|
+
additionalProperties: false,
|
|
284
|
+
},
|
|
285
|
+
execute: async (args) => {
|
|
286
|
+
try {
|
|
287
|
+
const code = readString(args, "code");
|
|
288
|
+
const percent = readOptionalNumber(args, "percent");
|
|
289
|
+
const amountPln = readOptionalNumber(args, "amountPln");
|
|
290
|
+
if ((percent === undefined) === (amountPln === undefined)) {
|
|
291
|
+
throw new ArgumentError("Podaj dokładnie jedno: percent albo amountPln.");
|
|
292
|
+
}
|
|
293
|
+
if (percent !== undefined && (!Number.isInteger(percent) || percent < 1 || percent > 100)) {
|
|
294
|
+
throw new ArgumentError("Rabat procentowy musi być liczbą całkowitą z zakresu 1-100.");
|
|
295
|
+
}
|
|
296
|
+
if (amountPln !== undefined && (!Number.isInteger(amountPln) || amountPln < 1)) {
|
|
297
|
+
throw new ArgumentError("Rabat kwotowy musi być dodatnią liczbą całkowitą złotych.");
|
|
298
|
+
}
|
|
299
|
+
const scopeServiceId = readOptionalString(args, "scopeServiceId");
|
|
300
|
+
await assertKnownService(port, scopeServiceId);
|
|
301
|
+
const value = percent !== undefined ? `${percent}%` : `${amountPln} zł`;
|
|
302
|
+
const scope = scopeServiceId ? "na jedną usługę" : "na cały salon";
|
|
303
|
+
if (!readBoolean(args, "confirmed")) {
|
|
304
|
+
return needsConfirmation(`Dodam kod rabatowy „${code}”: ${value}, ${scope}.`, { ...args, confirmed: true });
|
|
305
|
+
}
|
|
306
|
+
await port.addDiscountCode({ code, percent, amountPln, scopeServiceId });
|
|
307
|
+
return ok(`Dodano kod rabatowy „${code}” (${value}, ${scope}).`);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (error instanceof ArgumentError)
|
|
311
|
+
return err("invalid_arguments", error.message);
|
|
312
|
+
return toToolError(error, "Nie udało się dodać kodu rabatowego.");
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function removeDiscountCodeTool(port) {
|
|
318
|
+
return {
|
|
319
|
+
name: "remove_discount_code",
|
|
320
|
+
progress: "Usuwam kod rabatowy…",
|
|
321
|
+
description: "Usuwa kod rabatowy salonu. Kanister odmówi, dopóki jakakolwiek promocja ten kod rozgłasza — wtedy najpierw zmień promocję.",
|
|
322
|
+
parameters: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
code: { type: "string", description: "Treść kodu z list_discount_codes." },
|
|
326
|
+
confirmed: CONFIRMED_FIELD,
|
|
327
|
+
},
|
|
328
|
+
required: ["code", "confirmed"],
|
|
329
|
+
additionalProperties: false,
|
|
330
|
+
},
|
|
331
|
+
// No confirmation phrase here, unlike a service or a promo: a removed code can simply be
|
|
332
|
+
// added back with the same text, and nothing else in the salon points at it.
|
|
333
|
+
execute: async (args) => {
|
|
334
|
+
try {
|
|
335
|
+
const code = readString(args, "code");
|
|
336
|
+
const codes = await port.listDiscountCodes();
|
|
337
|
+
if (!codes.some((entry) => entry.code === code))
|
|
338
|
+
return err("not_found", `Salon nie ma kodu „${code}”.`);
|
|
339
|
+
if (!readBoolean(args, "confirmed")) {
|
|
340
|
+
return needsConfirmation(`Usunę kod rabatowy „${code}”.`, { ...args, confirmed: true });
|
|
341
|
+
}
|
|
342
|
+
await port.removeDiscountCode(code);
|
|
343
|
+
return ok(`Usunięto kod rabatowy „${code}”.`);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (error instanceof ArgumentError)
|
|
347
|
+
return err("invalid_arguments", error.message);
|
|
348
|
+
return toToolError(error, "Nie udało się usunąć kodu rabatowego.");
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Builds the whole promo record. As with a service, `updatePromo` replaces everything — including
|
|
355
|
+
* the music render — so an edit merges onto the current promo and carries `music` through
|
|
356
|
+
* untouched.
|
|
357
|
+
*/
|
|
358
|
+
async function buildPromoInput(port, args, current) {
|
|
359
|
+
const name = current ? (readOptionalString(args, "name") ?? current.name) : readString(args, "name");
|
|
360
|
+
if (name.length > MAX_PROMO_NAME_LENGTH) {
|
|
361
|
+
throw new ArgumentError(`Nazwa promocji może mieć najwyżej ${MAX_PROMO_NAME_LENGTH} znaków.`);
|
|
362
|
+
}
|
|
363
|
+
const assetId = current ? (readOptionalString(args, "assetId") ?? current.assetId) : readString(args, "assetId");
|
|
364
|
+
await assertUsableMedia(port, assetId, current?.assetId);
|
|
365
|
+
// A music promo's media IS the render made from its source; swapping it would leave the music
|
|
366
|
+
// record pointing at a different asset, which the canister then refuses. Say so plainly
|
|
367
|
+
// instead of letting the owner discover it as a validation error.
|
|
368
|
+
if (current?.hasMusic && assetId !== current.assetId) {
|
|
369
|
+
throw new ArgumentError("Ta promocja jest zmontowana z muzyką — materiału nie da się podmienić w rozmowie. Zrób to w panelu.");
|
|
370
|
+
}
|
|
371
|
+
const textLines = readOptionalStringArray(args, "textLines") ?? current?.textLines ?? [];
|
|
372
|
+
if (textLines.length > MAX_TEXT_LINES)
|
|
373
|
+
throw new ArgumentError(`Promocja może mieć najwyżej ${MAX_TEXT_LINES} linie tekstu.`);
|
|
374
|
+
const chips = readOptionalStringArray(args, "chips") ?? current?.chips ?? [];
|
|
375
|
+
if (chips.length > MAX_CHIPS)
|
|
376
|
+
throw new ArgumentError(`Promocja może mieć najwyżej ${MAX_CHIPS} etykiety.`);
|
|
377
|
+
const buttonLabel = readOptionalString(args, "buttonLabel") ?? current?.buttonLabel;
|
|
378
|
+
const discountCode = readOptionalString(args, "discountCode") ?? current?.discountCode;
|
|
379
|
+
await assertKnownDiscountCode(port, discountCode, current?.discountCode);
|
|
380
|
+
const targetServiceId = readOptionalString(args, "targetServiceId") ?? current?.targetServiceId;
|
|
381
|
+
await assertKnownService(port, targetServiceId);
|
|
382
|
+
// `music` is never model-visible; it is read back from the current promo so an edit cannot
|
|
383
|
+
// silently undo a render.
|
|
384
|
+
return { name, assetId, textLines, chips, buttonLabel, discountCode, targetServiceId, music: current?.music };
|
|
385
|
+
}
|
|
386
|
+
async function assertUsableMedia(port, assetId, currentAssetId) {
|
|
387
|
+
if (assetId === currentAssetId)
|
|
388
|
+
return;
|
|
389
|
+
const media = await port.listMedia();
|
|
390
|
+
const asset = media.find((item) => item.assetId === assetId);
|
|
391
|
+
if (!asset) {
|
|
392
|
+
throw new ArgumentError(`W bibliotece salonu nie ma gotowego materiału o id ${assetId}. Użyj list_media.`);
|
|
393
|
+
}
|
|
394
|
+
if (asset.kind === "audio")
|
|
395
|
+
throw new ArgumentError("Promocji nie da się założyć na ścieżce dźwiękowej — wybierz zdjęcie albo film.");
|
|
396
|
+
}
|
|
397
|
+
async function assertKnownDiscountCode(port, code, currentCode) {
|
|
398
|
+
if (code === undefined || code === currentCode)
|
|
399
|
+
return;
|
|
400
|
+
const codes = await port.listDiscountCodes();
|
|
401
|
+
if (!codes.some((entry) => entry.code === code)) {
|
|
402
|
+
throw new ArgumentError(`Salon nie ma kodu rabatowego „${code}”. Najpierw dodaj go przez add_discount_code.`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function assertKnownService(port, serviceId) {
|
|
406
|
+
if (serviceId === undefined)
|
|
407
|
+
return;
|
|
408
|
+
const services = await port.listServices();
|
|
409
|
+
if (!services.some((service) => service.id === serviceId)) {
|
|
410
|
+
throw new ArgumentError(`W tym salonie nie ma usługi o id ${serviceId}. Użyj list_services.`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function describePromo(port, input) {
|
|
414
|
+
const media = await port.listMedia();
|
|
415
|
+
const asset = media.find((item) => item.assetId === input.assetId);
|
|
416
|
+
const parts = [`„${input.name}”`, `materiał: ${asset ? asset.name : input.assetId}`];
|
|
417
|
+
if (input.textLines.length > 0)
|
|
418
|
+
parts.push(`tekst: ${input.textLines.join(" / ")}`);
|
|
419
|
+
if (input.chips.length > 0)
|
|
420
|
+
parts.push(`etykiety: ${input.chips.join(", ")}`);
|
|
421
|
+
if (input.buttonLabel)
|
|
422
|
+
parts.push(`przycisk: ${input.buttonLabel}`);
|
|
423
|
+
if (input.discountCode)
|
|
424
|
+
parts.push(`kod: ${input.discountCode}`);
|
|
425
|
+
if (input.targetServiceId) {
|
|
426
|
+
const services = await port.listServices();
|
|
427
|
+
const service = services.find((candidate) => candidate.id === input.targetServiceId);
|
|
428
|
+
parts.push(`prowadzi do usługi: ${service ? service.name : input.targetServiceId}`);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
parts.push("prowadzi do salonu");
|
|
432
|
+
}
|
|
433
|
+
return parts.join(", ");
|
|
434
|
+
}
|
|
435
|
+
async function findPromo(port, promoId) {
|
|
436
|
+
const promos = await port.listPromos();
|
|
437
|
+
return promos.find((promo) => promo.id === promoId) ?? null;
|
|
438
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../../src/tools/schedule.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAyB,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAA8B,KAAK,SAAS,EAAmB,MAAM,aAAa,CAAC;AAqB1F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,EAAE,CASpE"}
|