@solumflow-app/crm-mcp-server 0.7.0 → 0.9.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 +16 -4
- package/dist/index.js +665 -15
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
import { createClient } from "@solumflow-app/crm-client";
|
|
7
7
|
|
|
8
8
|
// package.json
|
|
9
|
-
var version = "0.
|
|
9
|
+
var version = "0.9.0";
|
|
10
10
|
|
|
11
11
|
// src/catalog-migration-tools.ts
|
|
12
12
|
import { z as z4 } from "zod";
|
|
@@ -506,8 +506,15 @@ var page = {
|
|
|
506
506
|
function more(nextCursor) {
|
|
507
507
|
return nextCursor ? `More on the next page: cursor ${nextCursor}` : "";
|
|
508
508
|
}
|
|
509
|
-
function money(
|
|
510
|
-
return `${(
|
|
509
|
+
function money(cents3, currency2) {
|
|
510
|
+
return `${(cents3 / 100).toFixed(2)} ${currency2.toUpperCase()}`;
|
|
511
|
+
}
|
|
512
|
+
function fieldsNotOnPageNote(slugs) {
|
|
513
|
+
if (!slugs?.length) {
|
|
514
|
+
return "";
|
|
515
|
+
}
|
|
516
|
+
const count = slugs.length === 1 ? "1 public field has" : `${slugs.length} public fields have`;
|
|
517
|
+
return ` ${count} a value but ${slugs.length === 1 ? "is" : "are"} not on this product's page: ${slugs.join(", ")}; send them in publicFields to show them.`;
|
|
511
518
|
}
|
|
512
519
|
|
|
513
520
|
// src/catalog-tools.ts
|
|
@@ -865,7 +872,7 @@ function registerCatalogMigrationTools(server, client) {
|
|
|
865
872
|
(file) => file.status === "pending"
|
|
866
873
|
);
|
|
867
874
|
return answer(
|
|
868
|
-
`${product.name} (${product.slug || "no address"}), ${product.status} (${product.availability}); ${Object.keys(product.fields).length} fields, ${product.categories.length} categories, ${product.images.length} pictures; files: ${pending.length} pending, ${refused.length} refused${refused.length ? ` (${refused.map((file) => `${file.ref}: ${file.reason}`).join("; ")})` : ""}
|
|
875
|
+
`${product.name} (${product.slug || "no address"}), ${product.status} (${product.availability}); ${Object.keys(product.fields).length} fields, ${product.categories.length} categories, ${product.images.length} pictures; files: ${pending.length} pending, ${refused.length} refused${refused.length ? ` (${refused.map((file) => `${file.ref}: ${file.reason}`).join("; ")})` : ""}.` + fieldsNotOnPageNote(product.fieldsNotOnPage),
|
|
869
876
|
product
|
|
870
877
|
);
|
|
871
878
|
} catch (error) {
|
|
@@ -899,12 +906,15 @@ function registerCatalogMigrationTools(server, client) {
|
|
|
899
906
|
target(object2)
|
|
900
907
|
);
|
|
901
908
|
const count = (kind) => outcomes.filter((outcome) => outcome.outcome === kind).length;
|
|
909
|
+
const offPage = new Set(
|
|
910
|
+
outcomes.flatMap((outcome) => outcome.product?.fieldsNotOnPage ?? [])
|
|
911
|
+
);
|
|
902
912
|
const refusals = outcomes.filter((outcome) => outcome.outcome === "refused").map(
|
|
903
913
|
(outcome) => `- ${outcome.externalRef}: ${outcome.error?.message ?? "refused"}` + (outcome.error?.fields.length ? ` (${outcome.error.fields.map((field) => `${field.path}: ${field.message}`).join("; ")})` : "")
|
|
904
914
|
);
|
|
905
915
|
return answer(
|
|
906
916
|
[
|
|
907
|
-
`${count("created")} made, ${count("changed")} changed, ${count("refused")} refused, ${count("skipped")} skipped
|
|
917
|
+
`${count("created")} made, ${count("changed")} changed, ${count("refused")} refused, ${count("skipped")} skipped.` + (offPage.size ? ` Some products hold public fields that are not on their page (${[...offPage].join(", ")}); send them in publicFields to show them.` : ""),
|
|
908
918
|
...refusals
|
|
909
919
|
].join("\n"),
|
|
910
920
|
outcomes
|
|
@@ -1074,7 +1084,7 @@ function registerCatalogWriteTools(server, client) {
|
|
|
1074
1084
|
);
|
|
1075
1085
|
const verb = product.created ? "Made" : "Changed";
|
|
1076
1086
|
return answer(
|
|
1077
|
-
`${verb} ${product.name} (${product.slug ?? "no address"}), ${product.availability}; ${product.variants.length} variants; pictures ${product.images}
|
|
1087
|
+
`${verb} ${product.name} (${product.slug ?? "no address"}), ${product.availability}; ${product.variants.length} variants; pictures ${product.images}.` + fieldsNotOnPageNote(product.fieldsNotOnPage),
|
|
1078
1088
|
product
|
|
1079
1089
|
);
|
|
1080
1090
|
} catch (error) {
|
|
@@ -1270,7 +1280,11 @@ var CONFIGURATOR_LIMITS = {
|
|
|
1270
1280
|
expandedParts: 200,
|
|
1271
1281
|
repeatCount: 12,
|
|
1272
1282
|
repeatDepth: 2,
|
|
1273
|
-
annotations: 30
|
|
1283
|
+
annotations: 30,
|
|
1284
|
+
roomWallOptions: 12,
|
|
1285
|
+
roomFloorOptions: 8,
|
|
1286
|
+
focus: 60,
|
|
1287
|
+
focusNames: 20
|
|
1274
1288
|
};
|
|
1275
1289
|
function jsonByteLength(value) {
|
|
1276
1290
|
return new TextEncoder().encode(JSON.stringify(value)).length + jsonbSpacing(value);
|
|
@@ -1639,8 +1653,10 @@ var PRIMITIVE_PARAMS = {
|
|
|
1639
1653
|
// A quarter of an elliptical shell in plan view, filling the block's width
|
|
1640
1654
|
// and depth; `mirror` 0 bends it towards the right, 1 towards the left.
|
|
1641
1655
|
arc_panel: { required: ["thickness"], optional: ["mirror"] },
|
|
1642
|
-
// A thin strip that gives light.
|
|
1643
|
-
|
|
1656
|
+
// A thin strip that gives light. `facing` is where the light goes: 0 down
|
|
1657
|
+
// (the default: under a shelf, under a floating cabinet), 1 up, 2 forwards
|
|
1658
|
+
// into the room, 3 back against the wall.
|
|
1659
|
+
led_strip: { required: [], optional: ["facing"] },
|
|
1644
1660
|
// A wall with a rectangular opening, measured from the block's own corner.
|
|
1645
1661
|
niche_panel: {
|
|
1646
1662
|
required: ["niche_x", "niche_y", "niche_width", "niche_height"],
|
|
@@ -1656,13 +1672,24 @@ var PRIMITIVE_PARAMS = {
|
|
|
1656
1672
|
};
|
|
1657
1673
|
var CONFIGURATOR_MODEL_PATH_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.glb$/;
|
|
1658
1674
|
var CONFIGURATOR_FOLLOWS_PRESET = "preset";
|
|
1675
|
+
var MOTION_GROUP_PATTERN = /^[a-z](?:[a-z0-9_]|\{[ij]\}){0,39}$/;
|
|
1676
|
+
var MOTION_KINDS = ["hinge", "slide"];
|
|
1677
|
+
var MOTION_EDGES = ["bottom", "top", "left", "right"];
|
|
1678
|
+
var GeometryMotionSchema = z5.strictObject({
|
|
1679
|
+
group: z5.string().regex(MOTION_GROUP_PATTERN, "configurator.invalidName"),
|
|
1680
|
+
kind: z5.enum(MOTION_KINDS),
|
|
1681
|
+
edge: z5.enum(MOTION_EDGES).optional(),
|
|
1682
|
+
angle: FormulaSchema.optional(),
|
|
1683
|
+
distance: FormulaSchema.optional()
|
|
1684
|
+
});
|
|
1659
1685
|
var PrimitiveNodeFields = {
|
|
1660
1686
|
id: ConfiguratorNameSchema,
|
|
1661
1687
|
slot: ConfiguratorNameSchema,
|
|
1662
1688
|
size: Vector3Schema,
|
|
1663
1689
|
at: Vector3Schema,
|
|
1664
1690
|
visible: FormulaSchema.optional(),
|
|
1665
|
-
params: z5.record(ConfiguratorNameSchema, FormulaSchema).optional()
|
|
1691
|
+
params: z5.record(ConfiguratorNameSchema, FormulaSchema).optional(),
|
|
1692
|
+
motion: GeometryMotionSchema.optional()
|
|
1666
1693
|
};
|
|
1667
1694
|
var PrimitiveNodeSchema = z5.strictObject({
|
|
1668
1695
|
...PrimitiveNodeFields,
|
|
@@ -1752,6 +1779,26 @@ function geometryDocumentSchema(modelFile) {
|
|
|
1752
1779
|
});
|
|
1753
1780
|
}
|
|
1754
1781
|
var GeometryDocumentSchema = geometryDocumentSchema(ModelPathSchema);
|
|
1782
|
+
function motionNames(nodes) {
|
|
1783
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1784
|
+
const groups = /* @__PURE__ */ new Set();
|
|
1785
|
+
const walk = (list) => {
|
|
1786
|
+
for (const node of list) {
|
|
1787
|
+
ids.add(node.id);
|
|
1788
|
+
if (node.type === "repeat") {
|
|
1789
|
+
walk(node.children);
|
|
1790
|
+
} else if (node.motion) {
|
|
1791
|
+
groups.add(node.motion.group);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
walk(nodes);
|
|
1796
|
+
return { ids, groups };
|
|
1797
|
+
}
|
|
1798
|
+
function motionGroupMatches(template, name) {
|
|
1799
|
+
const pattern = template.split(/\{[ij]\}/).join("[1-9][0-9]?");
|
|
1800
|
+
return new RegExp(`^${pattern}$`).test(name);
|
|
1801
|
+
}
|
|
1755
1802
|
function choiceModelBlocks(nodes) {
|
|
1756
1803
|
const found = [];
|
|
1757
1804
|
const walk = (list) => {
|
|
@@ -1991,6 +2038,32 @@ function geometryDocumentProblems(document, context = {}) {
|
|
|
1991
2038
|
}
|
|
1992
2039
|
checkFormula2(formula, [...nodePath, "params", key], scope);
|
|
1993
2040
|
}
|
|
2041
|
+
if (node.motion) {
|
|
2042
|
+
const motion = node.motion;
|
|
2043
|
+
const motionPath = [...nodePath, "motion"];
|
|
2044
|
+
if (motion.angle !== void 0) {
|
|
2045
|
+
checkFormula2(motion.angle, [...motionPath, "angle"], scope);
|
|
2046
|
+
}
|
|
2047
|
+
if (motion.distance !== void 0) {
|
|
2048
|
+
checkFormula2(motion.distance, [...motionPath, "distance"], scope);
|
|
2049
|
+
}
|
|
2050
|
+
for (const placeholder of motion.group.match(/\{([ij])\}/g) ?? []) {
|
|
2051
|
+
if (!scope.has(placeholder.slice(1, 2))) {
|
|
2052
|
+
problems.push(
|
|
2053
|
+
issue([...motionPath, "group"], "configurator.unknownName", {
|
|
2054
|
+
name: placeholder
|
|
2055
|
+
})
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if (motion.kind === "hinge" && motion.distance !== void 0 || motion.kind === "slide" && (motion.edge !== void 0 || motion.angle !== void 0)) {
|
|
2060
|
+
problems.push(
|
|
2061
|
+
issue(motionPath, "configurator.motionMixed", {
|
|
2062
|
+
kind: motion.kind
|
|
2063
|
+
})
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
1994
2067
|
if (node.follows !== void 0 && context.parameters) {
|
|
1995
2068
|
problems.push(
|
|
1996
2069
|
...followProblems(node.follows, nodePath, {
|
|
@@ -2039,6 +2112,34 @@ function followProblems(follows, path, context) {
|
|
|
2039
2112
|
|
|
2040
2113
|
// ../features/product-configurator/src/shared/configurator-structure.schema.ts
|
|
2041
2114
|
import { z as z6 } from "zod";
|
|
2115
|
+
|
|
2116
|
+
// ../features/product-configurator/src/shared/room-palette.ts
|
|
2117
|
+
var ROOM_WALL_PALETTE = [
|
|
2118
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2119
|
+
{ code: "gebroken-wit", label: "Gebroken wit", color: "#ece8e1" },
|
|
2120
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2121
|
+
{ code: "warm-grijs", label: "Warm grijs", color: "#b9b2a8" },
|
|
2122
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2123
|
+
{ code: "zand", label: "Zand", color: "#d6c5a8" },
|
|
2124
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2125
|
+
{ code: "saliegroen", label: "Saliegroen", color: "#a7b29c" },
|
|
2126
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2127
|
+
{ code: "nachtblauw", label: "Nachtblauw", color: "#2f3a4c" },
|
|
2128
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2129
|
+
{ code: "antraciet", label: "Antraciet", color: "#3d3d3f" }
|
|
2130
|
+
];
|
|
2131
|
+
var ROOM_FLOOR_PALETTE = [
|
|
2132
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2133
|
+
{ code: "licht-eiken", label: "Licht eiken", color: "#c8a77e" },
|
|
2134
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2135
|
+
{ code: "donker-eiken", label: "Donker eiken", color: "#6b4f38" },
|
|
2136
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2137
|
+
{ code: "betongrijs", label: "Betongrijs", color: "#9a9893" },
|
|
2138
|
+
// i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
|
|
2139
|
+
{ code: "warm-beige", label: "Warm beige tegel", color: "#d9cbb7" }
|
|
2140
|
+
];
|
|
2141
|
+
|
|
2142
|
+
// ../features/product-configurator/src/shared/configurator-structure.schema.ts
|
|
2042
2143
|
var LabelSchema = z6.string().trim().min(1).max(80);
|
|
2043
2144
|
var CONFIGURATOR_TEXTURE_PATH_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(webp|jpg|jpeg)$/;
|
|
2044
2145
|
var TexturePathSchema = z6.string().regex(CONFIGURATOR_TEXTURE_PATH_PATTERN, "configurator.invalidTexturePath");
|
|
@@ -2096,6 +2197,13 @@ var materialSchema = (picture) => z6.strictObject({
|
|
|
2096
2197
|
opacity: z6.number().min(0.05).max(1).optional(),
|
|
2097
2198
|
/** Gives light rather than reflecting it: a flame, a LED. */
|
|
2098
2199
|
emissive: z6.boolean().optional(),
|
|
2200
|
+
/**
|
|
2201
|
+
* The colour temperature of a light, in kelvin: a LED strip sold as
|
|
2202
|
+
* 3000 K glows and lights in that colour rather than in `color`, which
|
|
2203
|
+
* stays the swatch's fallback. A strip that can do several (2700-6500 K)
|
|
2204
|
+
* names the one to show: 4000 K.
|
|
2205
|
+
*/
|
|
2206
|
+
kelvin: z6.number().int().min(1500).max(1e4).optional(),
|
|
2099
2207
|
priceGroup: ConfiguratorCodeSchema.optional()
|
|
2100
2208
|
});
|
|
2101
2209
|
var ConfiguratorChoiceListSchema = choiceListSchema(TexturePathSchema);
|
|
@@ -2176,9 +2284,37 @@ var CONFIGURATOR_SCENE_LIGHTINGS = [
|
|
|
2176
2284
|
"daylight"
|
|
2177
2285
|
];
|
|
2178
2286
|
var CONFIGURATOR_SCENE_EDGES = ["soft", "sharp"];
|
|
2287
|
+
var RoomColorSchema = z6.strictObject({
|
|
2288
|
+
code: ConfiguratorCodeSchema,
|
|
2289
|
+
label: LabelSchema,
|
|
2290
|
+
color: z6.string().regex(/^#[0-9a-f]{6}$/i, "configurator.invalidColor")
|
|
2291
|
+
});
|
|
2292
|
+
var roomSurfaceSchema = (max) => z6.strictObject({
|
|
2293
|
+
/** The code shown first; absent is the first option. */
|
|
2294
|
+
default: ConfiguratorCodeSchema.optional(),
|
|
2295
|
+
/** Absent offers the package's own palette (`room-palette.ts`). */
|
|
2296
|
+
options: z6.array(RoomColorSchema).min(1).max(max).optional()
|
|
2297
|
+
});
|
|
2298
|
+
var ConfiguratorRoomSchema = z6.strictObject({
|
|
2299
|
+
wall: roomSurfaceSchema(CONFIGURATOR_LIMITS.roomWallOptions).optional(),
|
|
2300
|
+
floor: roomSurfaceSchema(CONFIGURATOR_LIMITS.roomFloorOptions).optional(),
|
|
2301
|
+
/** Millimetres of room beside the furniture on each side; 1500 by default. */
|
|
2302
|
+
margin: z6.number().min(0).max(5e3).optional(),
|
|
2303
|
+
/** Millimetres of floor in front of the wall; 3500 by default. */
|
|
2304
|
+
depth: z6.number().min(1e3).max(1e4).optional()
|
|
2305
|
+
});
|
|
2179
2306
|
var ConfiguratorSceneSchema = z6.strictObject({
|
|
2180
2307
|
lighting: z6.enum(CONFIGURATOR_SCENE_LIGHTINGS).optional(),
|
|
2181
|
-
edges: z6.enum(CONFIGURATOR_SCENE_EDGES).optional()
|
|
2308
|
+
edges: z6.enum(CONFIGURATOR_SCENE_EDGES).optional(),
|
|
2309
|
+
room: ConfiguratorRoomSchema.optional()
|
|
2310
|
+
});
|
|
2311
|
+
var FocusNameSchema = z6.string().regex(/^[a-z][a-z0-9_]{0,39}$/, "configurator.invalidName");
|
|
2312
|
+
var ConfiguratorFocusSchema = z6.strictObject({
|
|
2313
|
+
param: ConfiguratorNameSchema,
|
|
2314
|
+
when: ConfiguratorConditionSchema.optional(),
|
|
2315
|
+
open: z6.array(FocusNameSchema).max(CONFIGURATOR_LIMITS.focusNames).optional(),
|
|
2316
|
+
frame: z6.array(FocusNameSchema).min(1).max(CONFIGURATOR_LIMITS.focusNames).optional(),
|
|
2317
|
+
night: z6.boolean().optional()
|
|
2182
2318
|
});
|
|
2183
2319
|
var structureObjectSchema = (files) => z6.strictObject({
|
|
2184
2320
|
schemaVersion: z6.literal(1),
|
|
@@ -2212,6 +2348,8 @@ var structureObjectSchema = (files) => z6.strictObject({
|
|
|
2212
2348
|
required: z6.boolean()
|
|
2213
2349
|
}).optional(),
|
|
2214
2350
|
rules: z6.array(ConfiguratorRuleSchema).max(CONFIGURATOR_LIMITS.rules),
|
|
2351
|
+
/** What the picture opens and looks at when a choice changes. */
|
|
2352
|
+
focus: z6.array(ConfiguratorFocusSchema).max(CONFIGURATOR_LIMITS.focus).optional(),
|
|
2215
2353
|
priceRules: z6.array(ConfiguratorPriceRuleSchema).max(CONFIGURATOR_LIMITS.priceRules)
|
|
2216
2354
|
});
|
|
2217
2355
|
var ConfiguratorStructureObjectSchema = structureObjectSchema({
|
|
@@ -2495,6 +2633,78 @@ function configuratorStructureProblems(structure) {
|
|
|
2495
2633
|
);
|
|
2496
2634
|
}
|
|
2497
2635
|
});
|
|
2636
|
+
const shape = motionNames(structure.geometry.root);
|
|
2637
|
+
const isGroup = (name) => [...shape.groups].some((template) => motionGroupMatches(template, name));
|
|
2638
|
+
(structure.focus ?? []).forEach((focus, index) => {
|
|
2639
|
+
const path = ["focus", index];
|
|
2640
|
+
if (!parameters.has(focus.param)) {
|
|
2641
|
+
problems.push(
|
|
2642
|
+
issue([...path, "param"], "configurator.unknownParam", {
|
|
2643
|
+
key: focus.param
|
|
2644
|
+
})
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
if (focus.when) {
|
|
2648
|
+
const conditionParam = parameters.get(focus.when.param);
|
|
2649
|
+
const valueProblem = conditionParam ? conditionValueProblem(
|
|
2650
|
+
focus.when.op,
|
|
2651
|
+
focus.when.value,
|
|
2652
|
+
conditionParam,
|
|
2653
|
+
lists,
|
|
2654
|
+
materials
|
|
2655
|
+
) : "configurator.unknownParam";
|
|
2656
|
+
if (valueProblem) {
|
|
2657
|
+
problems.push(
|
|
2658
|
+
issue([...path, "when"], valueProblem, { key: focus.when.param })
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
(focus.open ?? []).forEach((name, nameIndex) => {
|
|
2663
|
+
if (!isGroup(name)) {
|
|
2664
|
+
problems.push(
|
|
2665
|
+
issue(
|
|
2666
|
+
[...path, "open", nameIndex],
|
|
2667
|
+
"configurator.unknownMotionGroup",
|
|
2668
|
+
{
|
|
2669
|
+
name
|
|
2670
|
+
}
|
|
2671
|
+
)
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
(focus.frame ?? []).forEach((name, nameIndex) => {
|
|
2676
|
+
if (!shape.ids.has(name) && !isGroup(name)) {
|
|
2677
|
+
problems.push(
|
|
2678
|
+
issue([...path, "frame", nameIndex], "configurator.unknownFrame", {
|
|
2679
|
+
name
|
|
2680
|
+
})
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
});
|
|
2685
|
+
const room = structure.scene?.room;
|
|
2686
|
+
for (const surface of ["wall", "floor"]) {
|
|
2687
|
+
const own = room?.[surface];
|
|
2688
|
+
const path = ["scene", "room", surface];
|
|
2689
|
+
const codes = own?.options ? uniqueKeys(
|
|
2690
|
+
own.options,
|
|
2691
|
+
(option) => option.code,
|
|
2692
|
+
[...path, "options"],
|
|
2693
|
+
"code",
|
|
2694
|
+
problems
|
|
2695
|
+
) : new Set(
|
|
2696
|
+
(surface === "wall" ? ROOM_WALL_PALETTE : ROOM_FLOOR_PALETTE).map(
|
|
2697
|
+
(option) => option.code
|
|
2698
|
+
)
|
|
2699
|
+
);
|
|
2700
|
+
if (own?.default !== void 0 && !codes.has(own.default)) {
|
|
2701
|
+
problems.push(
|
|
2702
|
+
issue([...path, "default"], "configurator.unknownCode", {
|
|
2703
|
+
code: own.default
|
|
2704
|
+
})
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2498
2708
|
const geometryParameters = new Map(
|
|
2499
2709
|
structure.parameters.map((parameter) => [
|
|
2500
2710
|
parameter.key,
|
|
@@ -2952,6 +3162,13 @@ function onStep(value, step) {
|
|
|
2952
3162
|
}
|
|
2953
3163
|
|
|
2954
3164
|
// ../features/product-configurator/src/shared/expand-geometry.ts
|
|
3165
|
+
function motionGroupName(template, scope) {
|
|
3166
|
+
return template.replace(
|
|
3167
|
+
/\{([ij])\}/g,
|
|
3168
|
+
(_, index) => String((scope[index] ?? 0) + 1)
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
var DEFAULT_HINGE_ANGLE = 90;
|
|
2955
3172
|
function expandGeometry(document, inputs, chosen = { choices: {} }) {
|
|
2956
3173
|
const issues = [];
|
|
2957
3174
|
const parts = [];
|
|
@@ -3064,6 +3281,29 @@ function expandGeometry(document, inputs, chosen = { choices: {} }) {
|
|
|
3064
3281
|
}
|
|
3065
3282
|
params[key] = number2;
|
|
3066
3283
|
}
|
|
3284
|
+
let motion;
|
|
3285
|
+
if (node.motion) {
|
|
3286
|
+
const angle = node.motion.angle === void 0 ? DEFAULT_HINGE_ANGLE : evaluate(node.motion.angle, scope, [
|
|
3287
|
+
...nodePath,
|
|
3288
|
+
"motion",
|
|
3289
|
+
"angle"
|
|
3290
|
+
]);
|
|
3291
|
+
const distance = node.motion.distance === void 0 ? null : evaluate(node.motion.distance, scope, [
|
|
3292
|
+
...nodePath,
|
|
3293
|
+
"motion",
|
|
3294
|
+
"distance"
|
|
3295
|
+
]);
|
|
3296
|
+
if (angle === null || node.motion.distance !== void 0 && distance === null) {
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
motion = {
|
|
3300
|
+
group: motionGroupName(node.motion.group, scope),
|
|
3301
|
+
kind: node.motion.kind,
|
|
3302
|
+
edge: node.motion.edge ?? "bottom",
|
|
3303
|
+
angle,
|
|
3304
|
+
distance
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3067
3307
|
parts.push({
|
|
3068
3308
|
key: `${node.id}${keySuffix}`,
|
|
3069
3309
|
id: node.id,
|
|
@@ -3072,7 +3312,8 @@ function expandGeometry(document, inputs, chosen = { choices: {} }) {
|
|
|
3072
3312
|
size,
|
|
3073
3313
|
at,
|
|
3074
3314
|
params,
|
|
3075
|
-
...model === void 0 ? {} : { model }
|
|
3315
|
+
...model === void 0 ? {} : { model },
|
|
3316
|
+
...motion === void 0 ? {} : { motion }
|
|
3076
3317
|
});
|
|
3077
3318
|
if (parts.length > CONFIGURATOR_LIMITS.expandedParts) {
|
|
3078
3319
|
issues.push(
|
|
@@ -3691,15 +3932,422 @@ function registerServerInfoTool(server, info) {
|
|
|
3691
3932
|
);
|
|
3692
3933
|
}
|
|
3693
3934
|
|
|
3694
|
-
// src/
|
|
3935
|
+
// src/shop-setup-tools.ts
|
|
3695
3936
|
import { z as z11 } from "zod";
|
|
3937
|
+
var id = z11.string().uuid();
|
|
3938
|
+
var cents2 = z11.number().int().min(0).max(99999999);
|
|
3939
|
+
var region = z11.object({
|
|
3940
|
+
countryCode: z11.string().regex(/^[A-Z]{2}$/).describe("Two capital letters, ISO 3166 (NL, BE, DE)."),
|
|
3941
|
+
postcodeFrom: z11.string().max(20).nullable().default(null).describe("With postcodeTo: only this range of the country."),
|
|
3942
|
+
postcodeTo: z11.string().max(20).nullable().default(null)
|
|
3943
|
+
});
|
|
3944
|
+
var discount = z11.object({
|
|
3945
|
+
name: z11.string().min(1).max(120).describe("What the team sees it as."),
|
|
3946
|
+
code: z11.string().max(40).nullable().describe(
|
|
3947
|
+
"What a buyer types (upper-cased), or null for one that applies on its own."
|
|
3948
|
+
),
|
|
3949
|
+
effect: z11.enum([
|
|
3950
|
+
"tier_price",
|
|
3951
|
+
"percent_off_lines",
|
|
3952
|
+
"amount_off_lines",
|
|
3953
|
+
"percent_off_order",
|
|
3954
|
+
"amount_off_order",
|
|
3955
|
+
"free_shipping"
|
|
3956
|
+
]).describe(
|
|
3957
|
+
"tier_price: a lower price from tierMinQuantity of one product; *_lines: off each matching line; *_order: once off the whole order; free_shipping: no postage."
|
|
3958
|
+
),
|
|
3959
|
+
percent: z11.number().nullable().describe("For a percentage: above 0, at most 100."),
|
|
3960
|
+
amountCents: z11.number().int().nullable().describe("For an amount, in currency, as the account enters prices."),
|
|
3961
|
+
currency: z11.string().regex(/^[A-Z]{3}$/).nullable().describe("Required beside amountCents (EUR)."),
|
|
3962
|
+
tierMinQuantity: z11.number().int().nullable(),
|
|
3963
|
+
minSubtotalCents: z11.number().int().nullable().describe("Only from this basket value, before discount."),
|
|
3964
|
+
productIds: z11.array(id).max(500).describe("Empty: every product."),
|
|
3965
|
+
categoryIds: z11.array(id).max(100),
|
|
3966
|
+
objectIds: z11.array(id).max(20),
|
|
3967
|
+
startsAt: z11.string().datetime({ offset: true }).nullable(),
|
|
3968
|
+
endsAt: z11.string().datetime({ offset: true }).nullable(),
|
|
3969
|
+
maxUses: z11.number().int().positive().nullable(),
|
|
3970
|
+
maxUsesPerEmail: z11.number().int().positive().nullable(),
|
|
3971
|
+
combinable: z11.boolean().describe(
|
|
3972
|
+
"Whether it may count beside another discount on the same order."
|
|
3973
|
+
)
|
|
3974
|
+
});
|
|
3975
|
+
function registerShopSetupTools(server, client) {
|
|
3976
|
+
server.registerTool(
|
|
3977
|
+
"check_shop_readiness",
|
|
3978
|
+
{
|
|
3979
|
+
title: "What the shop still misses",
|
|
3980
|
+
description: "Everything that stops the shop from selling (blocker) or costs it sales (warning): no product for sale, products without a price, no shipping method, Stripe not connected or still in test mode, no revalidation webhook, no cookie banner, no visit tracking. Each finding names the tool that fixes it, or the screen in the app. Run it first, and again when you think you are done.",
|
|
3981
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
3982
|
+
},
|
|
3983
|
+
async () => {
|
|
3984
|
+
try {
|
|
3985
|
+
const readiness = await client.getShopReadiness();
|
|
3986
|
+
return answer(
|
|
3987
|
+
[
|
|
3988
|
+
readiness.ready ? "Nothing blocks a sale." : "The shop cannot sell yet.",
|
|
3989
|
+
...readiness.findings.map(describeFinding)
|
|
3990
|
+
].join("\n"),
|
|
3991
|
+
readiness
|
|
3992
|
+
);
|
|
3993
|
+
} catch (error) {
|
|
3994
|
+
return refusal(error, "shop:manage");
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
);
|
|
3998
|
+
server.registerTool(
|
|
3999
|
+
"list_shipping_methods",
|
|
4000
|
+
{
|
|
4001
|
+
title: "List shipping zones and methods",
|
|
4002
|
+
description: "Every shipping zone with the countries it covers and the methods under it (what each charges, whether it is on), and the shipping classes a flat rate may charge per class. Amounts in cents, as the account enters prices.",
|
|
4003
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
4004
|
+
},
|
|
4005
|
+
async () => {
|
|
4006
|
+
try {
|
|
4007
|
+
const setup = await client.getShippingSetup();
|
|
4008
|
+
return answer(
|
|
4009
|
+
setup.zones.length === 0 ? "No shipping zones yet: make one with upsert_shipping_zone." : setup.zones.map(describeZone).join("\n"),
|
|
4010
|
+
setup
|
|
4011
|
+
);
|
|
4012
|
+
} catch (error) {
|
|
4013
|
+
return refusal(error, "shop:manage");
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
);
|
|
4017
|
+
server.registerTool(
|
|
4018
|
+
"upsert_shipping_zone",
|
|
4019
|
+
{
|
|
4020
|
+
title: "Make or change a shipping zone",
|
|
4021
|
+
description: "A zone is a name and the countries (or postcode ranges) it covers. Without id a new zone; with one, that zone renamed and its places replaced by these. Answers the zone id, for upsert_shipping_method.",
|
|
4022
|
+
inputSchema: {
|
|
4023
|
+
id: id.optional().describe("The zone to change; leave out for a new one."),
|
|
4024
|
+
name: z11.string().min(1).max(80),
|
|
4025
|
+
regions: z11.array(region).max(300).default([])
|
|
4026
|
+
},
|
|
4027
|
+
annotations: { idempotentHint: true }
|
|
4028
|
+
},
|
|
4029
|
+
async (input) => {
|
|
4030
|
+
try {
|
|
4031
|
+
const zone = await client.upsertShippingZone(input);
|
|
4032
|
+
return answer(
|
|
4033
|
+
`${input.id ? "Changed" : "Made"} zone "${input.name}" (id ${zone.id}).`,
|
|
4034
|
+
zone
|
|
4035
|
+
);
|
|
4036
|
+
} catch (error) {
|
|
4037
|
+
return refusal(error, "shop:manage");
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
);
|
|
4041
|
+
server.registerTool(
|
|
4042
|
+
"upsert_shipping_method",
|
|
4043
|
+
{
|
|
4044
|
+
title: "Make or change a shipping method",
|
|
4045
|
+
description: "A way to post under a zone. flat_rate charges amountCents (and per class through classRates, noClassAmountCents for a product without one); free_shipping charges nothing, from freeThresholdCents of goods if given; local_pickup charges nothing. Without id a new method under zoneId; with one, that method changed -- its kind and zone are fixed once made.",
|
|
4046
|
+
inputSchema: {
|
|
4047
|
+
id: id.optional(),
|
|
4048
|
+
zoneId: id,
|
|
4049
|
+
kind: z11.enum(["flat_rate", "free_shipping", "local_pickup"]),
|
|
4050
|
+
name: z11.string().min(1).max(80).describe("What the buyer chooses."),
|
|
4051
|
+
enabled: z11.boolean().default(true),
|
|
4052
|
+
amountCents: cents2.default(0),
|
|
4053
|
+
noClassAmountCents: cents2.default(0),
|
|
4054
|
+
taxRateId: id.nullish(),
|
|
4055
|
+
freeThresholdCents: cents2.nullish(),
|
|
4056
|
+
classRates: z11.array(z11.object({ classId: id, amountCents: cents2 })).max(100).default([])
|
|
4057
|
+
},
|
|
4058
|
+
annotations: { idempotentHint: true }
|
|
4059
|
+
},
|
|
4060
|
+
async (input) => {
|
|
4061
|
+
try {
|
|
4062
|
+
const method = await client.upsertShippingMethod(input);
|
|
4063
|
+
return answer(
|
|
4064
|
+
`${input.id ? "Changed" : "Made"} method "${input.name}" (id ${method.id}).`,
|
|
4065
|
+
method
|
|
4066
|
+
);
|
|
4067
|
+
} catch (error) {
|
|
4068
|
+
return refusal(error, "shop:manage");
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
);
|
|
4072
|
+
server.registerTool(
|
|
4073
|
+
"list_discounts",
|
|
4074
|
+
{
|
|
4075
|
+
title: "List discounts",
|
|
4076
|
+
description: "The account discounts, newest first, at most 200: codes, the ones that apply on their own, and tier prices, with every condition and how many paid orders used each. Archived ones only with archived: true.",
|
|
4077
|
+
inputSchema: { archived: z11.boolean().optional() },
|
|
4078
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
4079
|
+
},
|
|
4080
|
+
async ({ archived }) => {
|
|
4081
|
+
try {
|
|
4082
|
+
const discounts = await client.getDiscounts({ archived });
|
|
4083
|
+
return answer(
|
|
4084
|
+
discounts.length === 0 ? "No discounts." : discounts.map(describeDiscount).join("\n"),
|
|
4085
|
+
discounts
|
|
4086
|
+
);
|
|
4087
|
+
} catch (error) {
|
|
4088
|
+
return refusal(error, "shop:manage");
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
);
|
|
4092
|
+
server.registerTool(
|
|
4093
|
+
"upsert_discount",
|
|
4094
|
+
{
|
|
4095
|
+
title: "Make or change a discount",
|
|
4096
|
+
description: "A code, a discount that applies on its own (code null), or a tier price. Every field is sent, null where it does not apply. Without id a new discount; with one, that discount replaced field by field. A code the account already has is refused (conflict). A discount is counted against maxUses when an order with it is paid.",
|
|
4097
|
+
inputSchema: {
|
|
4098
|
+
id: id.optional(),
|
|
4099
|
+
discount
|
|
4100
|
+
},
|
|
4101
|
+
annotations: { idempotentHint: true }
|
|
4102
|
+
},
|
|
4103
|
+
async (input) => {
|
|
4104
|
+
try {
|
|
4105
|
+
const made = await client.upsertDiscount(input);
|
|
4106
|
+
return answer(describeDiscount(made), made);
|
|
4107
|
+
} catch (error) {
|
|
4108
|
+
return refusal(error, "shop:manage");
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
);
|
|
4112
|
+
server.registerTool(
|
|
4113
|
+
"archive_discount",
|
|
4114
|
+
{
|
|
4115
|
+
title: "Archive a discount",
|
|
4116
|
+
description: "Stops a discount from giving anything to a new cart or order; orders that used it keep it. archived: false brings it back. Deleting is left to a person in the app.",
|
|
4117
|
+
inputSchema: {
|
|
4118
|
+
id,
|
|
4119
|
+
archived: z11.boolean().default(true)
|
|
4120
|
+
},
|
|
4121
|
+
annotations: { idempotentHint: true }
|
|
4122
|
+
},
|
|
4123
|
+
async ({ id: discountId, archived }) => {
|
|
4124
|
+
try {
|
|
4125
|
+
const result = await client.archiveDiscount(discountId, archived);
|
|
4126
|
+
return answer(
|
|
4127
|
+
archived ? "Archived." : "Brought back from the archive.",
|
|
4128
|
+
result
|
|
4129
|
+
);
|
|
4130
|
+
} catch (error) {
|
|
4131
|
+
return refusal(error, "shop:manage");
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
);
|
|
4135
|
+
server.registerTool(
|
|
4136
|
+
"get_payment_status",
|
|
4137
|
+
{
|
|
4138
|
+
title: "Can the shop take a payment",
|
|
4139
|
+
description: "Whether a Stripe account is connected and ready for cards, whether Stripe still asks for something, and whether the account is in test mode. Read only: connecting Stripe and switching to live happen in the app, at the screen named in connectIn -- tell the person.",
|
|
4140
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
4141
|
+
},
|
|
4142
|
+
async () => {
|
|
4143
|
+
try {
|
|
4144
|
+
const status = await client.getPaymentStatus();
|
|
4145
|
+
const sentence = !status.connected ? `Stripe is not connected (${status.status}). A person connects it in the app: ${status.connectIn}.` : [
|
|
4146
|
+
"Stripe is connected",
|
|
4147
|
+
status.testMode ? "in test mode: orders are test orders and no money moves" : "and live",
|
|
4148
|
+
status.requirementsDue ? "Stripe still asks for details" : ""
|
|
4149
|
+
].filter(Boolean).join(", ") + ".";
|
|
4150
|
+
return answer(sentence, status);
|
|
4151
|
+
} catch (error) {
|
|
4152
|
+
return refusal(error, "shop:manage");
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
);
|
|
4156
|
+
server.registerTool(
|
|
4157
|
+
"list_api_webhooks",
|
|
4158
|
+
{
|
|
4159
|
+
title: "List webhook addresses",
|
|
4160
|
+
description: "The addresses the account tells about changes (products, categories, events, orders), with whether each is on, paused after failures, and how its last delivery went. The signing key is never shown again.",
|
|
4161
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
4162
|
+
},
|
|
4163
|
+
async () => {
|
|
4164
|
+
try {
|
|
4165
|
+
const hooks = await client.getApiWebhooks();
|
|
4166
|
+
return answer(
|
|
4167
|
+
hooks.length === 0 ? "No webhook addresses." : hooks.map(
|
|
4168
|
+
(hook) => `- ${hook.name} (id ${hook.id}): ${hook.url}, ${hook.events.join(", ")}` + (hook.paused ? `, paused after ${hook.consecutiveFailures} failures` : hook.active ? "" : ", off")
|
|
4169
|
+
).join("\n"),
|
|
4170
|
+
hooks
|
|
4171
|
+
);
|
|
4172
|
+
} catch (error) {
|
|
4173
|
+
return refusal(error, "shop:manage");
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
);
|
|
4177
|
+
server.registerTool(
|
|
4178
|
+
"upsert_api_webhook",
|
|
4179
|
+
{
|
|
4180
|
+
title: "Make or change a webhook address",
|
|
4181
|
+
description: "Point the account at the site revalidation route (with the SDK: https://<site>/api/solumflow/revalidate) so its cached pages follow a product the moment it changes. Without id a new address: the answer carries its signing key once -- put it in the site environment as SOLUMFLOW_WEBHOOK_KEY, it cannot be read back. https only, never a private network.",
|
|
4182
|
+
inputSchema: {
|
|
4183
|
+
id: id.optional(),
|
|
4184
|
+
name: z11.string().min(1).max(80),
|
|
4185
|
+
url: z11.string().url().max(2048),
|
|
4186
|
+
events: z11.array(z11.string().min(1).max(60)).min(1).max(20).describe(
|
|
4187
|
+
"For a shop: product.changed, product.deleted, category.changed."
|
|
4188
|
+
)
|
|
4189
|
+
},
|
|
4190
|
+
annotations: { idempotentHint: false }
|
|
4191
|
+
},
|
|
4192
|
+
async (input) => {
|
|
4193
|
+
try {
|
|
4194
|
+
const result = await client.upsertApiWebhook(input);
|
|
4195
|
+
return answer(
|
|
4196
|
+
result.signingSecret ? `Made address ${result.id}. Its signing key is in this answer and nowhere else: set it as SOLUMFLOW_WEBHOOK_KEY in the site environment now.` : `Changed address ${result.id}.`,
|
|
4197
|
+
result
|
|
4198
|
+
);
|
|
4199
|
+
} catch (error) {
|
|
4200
|
+
return refusal(error, "shop:manage");
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
);
|
|
4204
|
+
}
|
|
4205
|
+
function describeFinding(finding) {
|
|
4206
|
+
const examples = finding.examples?.length ? ` (${finding.examples.join(", ")})` : "";
|
|
4207
|
+
return `- ${finding.severity}: ${finding.message}${examples} Fixed by ${finding.fixedBy}.`;
|
|
4208
|
+
}
|
|
4209
|
+
function describeZone(zone) {
|
|
4210
|
+
const where = zone.isFallback ? "everywhere else" : zone.regions.map((place) => place.countryCode).join(", ") || "no places";
|
|
4211
|
+
const methods = zone.methods.map(
|
|
4212
|
+
(method) => ` - ${method.name} (${method.kind}, id ${method.id})${method.enabled ? "" : ", off"}: ` + (method.kind === "flat_rate" ? `${(method.amountCents / 100).toFixed(2)}` : method.freeThresholdCents !== null ? `free from ${(method.freeThresholdCents / 100).toFixed(2)}` : "free")
|
|
4213
|
+
);
|
|
4214
|
+
return [`- ${zone.name} (id ${zone.id}): ${where}`, ...methods].join("\n");
|
|
4215
|
+
}
|
|
4216
|
+
function describeDiscount(entry) {
|
|
4217
|
+
const what = entry.percent !== null ? `${entry.percent}%` : entry.amountCents !== null && entry.currency ? money(entry.amountCents, entry.currency) : "";
|
|
4218
|
+
return `- ${entry.name} (id ${entry.id}): ${entry.code ?? "no code"}, ${entry.effect}${what ? ` ${what}` : ""}, used ${entry.timesUsed}x` + (entry.archived ? ", archived" : "");
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
// src/shop-test-order-tool.ts
|
|
4222
|
+
import { z as z12 } from "zod";
|
|
4223
|
+
var PAID_POLLS = 10;
|
|
4224
|
+
var PAID_POLL_MS = 1500;
|
|
4225
|
+
function registerShopTestOrderTool(server, client, options = {}) {
|
|
4226
|
+
const wait = options.wait ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
4227
|
+
server.registerTool(
|
|
4228
|
+
"run_test_order",
|
|
4229
|
+
{
|
|
4230
|
+
title: "Place and pay a test order",
|
|
4231
|
+
description: "Proves the shop end to end in Stripe test mode: puts one product in a cart, chooses postage, places the order, pays it with Stripe test card 4242, and reads the order back until it is paid (the invoice and the confirmation mail follow from there). Refused when the account is live -- no real money can move through this tool. Needs a key with shop:manage, cart:write, orders:write and orders:read.",
|
|
4232
|
+
inputSchema: {
|
|
4233
|
+
email: z12.string().email().describe(
|
|
4234
|
+
"Where the confirmation goes: the shop owner, never a customer."
|
|
4235
|
+
),
|
|
4236
|
+
productId: z12.string().uuid().optional().describe(
|
|
4237
|
+
"The product to buy; leave out for the first one in stock."
|
|
4238
|
+
),
|
|
4239
|
+
countryCode: z12.string().regex(/^[A-Z]{2}$/).default("NL").describe("Where it is posted, for a product that is posted."),
|
|
4240
|
+
postalCode: z12.string().max(20).optional()
|
|
4241
|
+
},
|
|
4242
|
+
annotations: { idempotentHint: false }
|
|
4243
|
+
},
|
|
4244
|
+
async (input) => {
|
|
4245
|
+
let scope = "shop:manage";
|
|
4246
|
+
try {
|
|
4247
|
+
const status = await client.getPaymentStatus();
|
|
4248
|
+
if (!status.connected) {
|
|
4249
|
+
return answer(
|
|
4250
|
+
`Stripe is not connected, so there is nothing to pay with. A person connects it in the app: ${status.connectIn}.`,
|
|
4251
|
+
status
|
|
4252
|
+
);
|
|
4253
|
+
}
|
|
4254
|
+
if (!status.testMode) {
|
|
4255
|
+
return answer(
|
|
4256
|
+
`The account is live: a test order would be a real one. Switch the account to Stripe test mode in the app first (${status.connectIn}), or prove the shop with a real purchase by hand.`,
|
|
4257
|
+
status
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4260
|
+
scope = "catalog:read";
|
|
4261
|
+
const productId = input.productId ?? await firstProductForSale();
|
|
4262
|
+
if (!productId) {
|
|
4263
|
+
return answer(
|
|
4264
|
+
"No product is for sale and in stock: there is nothing to order. check_shop_readiness says what is missing.",
|
|
4265
|
+
null
|
|
4266
|
+
);
|
|
4267
|
+
}
|
|
4268
|
+
scope = "cart:write";
|
|
4269
|
+
const handle = await client.createCart({ email: input.email });
|
|
4270
|
+
let cart = await client.addCartLine(handle.token, {
|
|
4271
|
+
productId,
|
|
4272
|
+
quantity: 1
|
|
4273
|
+
});
|
|
4274
|
+
if (cart.shipping.required) {
|
|
4275
|
+
cart = await client.setCartShipping(handle.token, {
|
|
4276
|
+
countryCode: input.countryCode,
|
|
4277
|
+
postalCode: input.postalCode ?? null
|
|
4278
|
+
});
|
|
4279
|
+
const method = cart.shipping.methods[0];
|
|
4280
|
+
if (!method) {
|
|
4281
|
+
return answer(
|
|
4282
|
+
`No shipping method delivers to ${input.countryCode}: add one with upsert_shipping_method.`,
|
|
4283
|
+
cart
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
cart = await client.setCartShipping(handle.token, {
|
|
4287
|
+
countryCode: input.countryCode,
|
|
4288
|
+
postalCode: input.postalCode ?? null,
|
|
4289
|
+
methodId: method.id
|
|
4290
|
+
});
|
|
4291
|
+
}
|
|
4292
|
+
if (cart.problem) {
|
|
4293
|
+
return answer(
|
|
4294
|
+
`The cart cannot be ordered: ${cart.problem.message}`,
|
|
4295
|
+
cart
|
|
4296
|
+
);
|
|
4297
|
+
}
|
|
4298
|
+
scope = "orders:write";
|
|
4299
|
+
const order = await client.submitCartOrder({
|
|
4300
|
+
buyer: { email: input.email },
|
|
4301
|
+
cartToken: handle.token,
|
|
4302
|
+
...cart.shipping.required ? {
|
|
4303
|
+
shippingAddress: {
|
|
4304
|
+
countryCode: input.countryCode,
|
|
4305
|
+
...input.postalCode ? { postalCode: input.postalCode } : {}
|
|
4306
|
+
}
|
|
4307
|
+
} : {}
|
|
4308
|
+
});
|
|
4309
|
+
scope = "shop:manage";
|
|
4310
|
+
const payment = await client.payTestOrder(order.id);
|
|
4311
|
+
scope = "orders:read";
|
|
4312
|
+
const settled = await readUntilPaid(order.id);
|
|
4313
|
+
const total = money(order.totalCents, order.currency);
|
|
4314
|
+
return answer(
|
|
4315
|
+
settled?.status === "paid" ? `Test order ${order.number} for ${total} is paid. The invoice and the confirmation mail to ${input.email} follow from here.` : `Test order ${order.number} for ${total}: Stripe says the payment ${payment.paymentStatus}, but the order still reads ${settled?.status ?? "unknown"}. The platform marks it paid when Stripe reaches its webhook; if that never happens, the webhook between Stripe and the platform is the place to look.`,
|
|
4316
|
+
{ order, payment, settled }
|
|
4317
|
+
);
|
|
4318
|
+
} catch (error) {
|
|
4319
|
+
return refusal(error, scope);
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
);
|
|
4323
|
+
async function firstProductForSale() {
|
|
4324
|
+
const list = await client.getProducts({ limit: 50 });
|
|
4325
|
+
return list.data.find(
|
|
4326
|
+
(product) => product.inStock && product.priceFromCents !== null
|
|
4327
|
+
)?.id;
|
|
4328
|
+
}
|
|
4329
|
+
async function readUntilPaid(orderId) {
|
|
4330
|
+
let order = null;
|
|
4331
|
+
for (let poll = 0; poll < PAID_POLLS; poll += 1) {
|
|
4332
|
+
order = await client.getOrder(orderId);
|
|
4333
|
+
if (order?.status === "paid") {
|
|
4334
|
+
return order;
|
|
4335
|
+
}
|
|
4336
|
+
await wait(PAID_POLL_MS);
|
|
4337
|
+
}
|
|
4338
|
+
return order;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
// src/tool-definitions-version.ts
|
|
4343
|
+
import { z as z13 } from "zod";
|
|
3696
4344
|
import { createHash } from "crypto";
|
|
3697
4345
|
function toolDefinitionsHash(tools) {
|
|
3698
4346
|
const definitions = [...tools].sort((left, right) => left.name.localeCompare(right.name)).map(({ name, config }) => ({
|
|
3699
4347
|
name,
|
|
3700
4348
|
title: config.title ?? null,
|
|
3701
4349
|
description: config.description ?? null,
|
|
3702
|
-
inputSchema: config.inputSchema ?
|
|
4350
|
+
inputSchema: config.inputSchema ? z13.toJSONSchema(z13.object(config.inputSchema), {
|
|
3703
4351
|
io: "input",
|
|
3704
4352
|
unrepresentable: "any"
|
|
3705
4353
|
}) : null,
|
|
@@ -3747,7 +4395,7 @@ function holdToolRegistrations(real) {
|
|
|
3747
4395
|
description: `${config.description ?? ""} Definitions version ${version2}: send it as schemaHash and the answer tells you when these tool definitions are older than the server.`,
|
|
3748
4396
|
inputSchema: {
|
|
3749
4397
|
...config.inputSchema,
|
|
3750
|
-
schemaHash:
|
|
4398
|
+
schemaHash: z13.string().max(64).optional().describe(
|
|
3751
4399
|
`The definitions version named in this tool's description (${version2}).`
|
|
3752
4400
|
)
|
|
3753
4401
|
}
|
|
@@ -3808,6 +4456,8 @@ async function main() {
|
|
|
3808
4456
|
registerCatalogMigrationTools(held.server, client);
|
|
3809
4457
|
registerEventTools(held.server, client);
|
|
3810
4458
|
registerConfiguratorTools(held.server, client);
|
|
4459
|
+
registerShopSetupTools(held.server, client);
|
|
4460
|
+
registerShopTestOrderTool(held.server, client);
|
|
3811
4461
|
held.publish();
|
|
3812
4462
|
await server.connect(new StdioServerTransport());
|
|
3813
4463
|
try {
|