continentept-mcp 0.4.0 → 0.6.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/CHANGELOG.md +36 -1
- package/README.md +1 -1
- package/dist/clients/storefront.js +1 -1
- package/dist/domain/models.d.ts +177 -37
- package/dist/domain/models.js +33 -8
- package/dist/domain/money.d.ts +17 -11
- package/dist/domain/money.js +25 -5
- package/dist/mcp/contracts.d.ts +166 -33
- package/dist/mcp/contracts.js +1 -1
- package/dist/mcp/guide.d.ts +2 -2
- package/dist/mcp/guide.js +3 -3
- package/dist/mcp/shopping.d.ts +4 -0
- package/dist/mcp/shopping.js +57 -28
- package/dist/parsers/cart.js +69 -21
- package/dist/parsers/catalog.d.ts +15 -5
- package/dist/parsers/catalog.js +56 -45
- package/dist/parsers/product.js +11 -6
- package/dist/services/cart.js +4 -1
- package/dist/services/catalog.d.ts +17 -4
- package/dist/services/catalog.js +3 -2
- package/docs/tools.md +55 -12
- package/docs/troubleshooting.md +1 -1
- package/package.json +7 -3
- package/skills/basket-planner/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] - 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Breaking:** `get_cart` returns `totals` instead of `total`: `products`, `bags`, `deposit`, `delivery`, `discounts` and `grandTotal`, the amount to pay. The old `total` left out the bags, so it was less than the amount to pay.
|
|
14
|
+
- **Breaking:** cart line prices are `{ amount, per }` price tags, like catalogue prices, plus a `lineTotal`. Products sold by the piece but priced by weight were shown as a price "each" when it was the price per kg.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Cart lines have a `kind`, `bag` for the shopping bags the website adds by itself and `product` for everything else.
|
|
19
|
+
- `search_products` and `get_product` return `recommendedPrice`, the recommended retail price (PVPR) the website shows beside the price on many promotions, so assistants can compare it with the selling price. ([#4](https://github.com/andredelgado/continente-mcp/pull/4), thanks [@Lozano-Miguel](https://github.com/Lozano-Miguel))
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Products sold by the piece but priced per kilogram (fruit, vegetables, meat, fish, counter cheese and ham) had no sale rules, so they could not be added to the cart. Their maximum is now read in kilograms, like the minimum and step, and converted to whole pieces.
|
|
24
|
+
- Cart quantities now have a unit. They were all reported as `unknown`; each line's unit now comes from its own sale rules, so a weighted line reads as pieces. A cart change is no longer confirmed by a line that reports another unit.
|
|
25
|
+
|
|
26
|
+
## [0.5.0] - 2026-10-15
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Searching for produce, meat and other products priced per kilogram failed with "Invalid money amount". Prices now record what they are for: per item, per piece, per kg or per litre.
|
|
31
|
+
- One unreadable search result no longer fails the whole search; the other results are returned as a partial result, and the unreadable one is described.
|
|
32
|
+
- Search results keep the website's relevance order. Products whose names merely resembled a favorite no longer jump ahead of better matches; exact favorites still come first.
|
|
33
|
+
- Favorites priced per kilogram are read correctly, and an unrecognised favorite price is reported as unknown instead of failing the refresh.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Breaking:** catalogue prices (`price` in search, favorites and `get_product`) are now `{ amount, per }`. `unitPriceText` is replaced by `secondaryPrice`, and the search field `similarity` is removed. Saved favorites are fetched again once after updating.
|
|
38
|
+
- Search results include `packageText`, the pack size as shown on the website.
|
|
39
|
+
- Sign-in errors say whether no credentials are configured or Continente did not accept them, with the next step for each case. The instructions present the `basket-planner` prompt as optional, for clients that support prompts.
|
|
40
|
+
- Browser engine updated to Playwright 1.64 (Chrome 156). After updating, the new Chromium downloads automatically on first use (about 150 MB, once).
|
|
41
|
+
|
|
9
42
|
## [0.4.0] - 2026-10-15
|
|
10
43
|
|
|
11
44
|
### Added
|
|
@@ -55,7 +88,9 @@ All notable changes to this project are documented here. The format follows
|
|
|
55
88
|
- Automatic sign-in from environment variables or a private credentials file, and a `login` command.
|
|
56
89
|
- The `basket-planner` skill for planning a shopping list and updating the basket.
|
|
57
90
|
|
|
58
|
-
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.
|
|
91
|
+
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.6.0...HEAD
|
|
92
|
+
[0.6.0]: https://github.com/andredelgado/continente-mcp/compare/v0.5.0...v0.6.0
|
|
93
|
+
[0.5.0]: https://github.com/andredelgado/continente-mcp/compare/v0.4.0...v0.5.0
|
|
59
94
|
[0.4.0]: https://github.com/andredelgado/continente-mcp/compare/v0.3.1...v0.4.0
|
|
60
95
|
[0.3.1]: https://github.com/andredelgado/continente-mcp/compare/v0.3.0...v0.3.1
|
|
61
96
|
[0.3.0]: https://github.com/andredelgado/continente-mcp/compare/v0.2.0...v0.3.0
|
package/README.md
CHANGED
|
@@ -119,7 +119,7 @@ Continente cart"* or *"What do I usually buy? Build me a list for this week."*
|
|
|
119
119
|
| `search_products` | Search the catalogue; favorites rank first |
|
|
120
120
|
| `get_product` | Price, availability and sale rules for one product |
|
|
121
121
|
| `list_favorites` | Your saved favorites (`refresh: true` fetches the latest) |
|
|
122
|
-
| `get_cart` | Cart lines, quantities and
|
|
122
|
+
| `get_cart` | Cart lines, quantities, bags and totals |
|
|
123
123
|
| `add_cart_item` | Add an amount on top of what is in the cart |
|
|
124
124
|
| `set_cart_item_quantity` | Make the cart hold exactly a quantity; zero removes |
|
|
125
125
|
| `remove_cart_item` | Remove a line from the cart |
|
|
@@ -93,7 +93,7 @@ export class CatalogClient {
|
|
|
93
93
|
break;
|
|
94
94
|
}
|
|
95
95
|
const previousCount = ids.size;
|
|
96
|
-
parsed.value.forEach(product => ids.add(product.productId));
|
|
96
|
+
parsed.value.products.forEach(product => ids.add(product.productId));
|
|
97
97
|
pages.push(html);
|
|
98
98
|
reportProgress(pages.length, 5, 'Reading catalogue pages');
|
|
99
99
|
const continuation = parseSearchPagination(html);
|
package/dist/domain/models.d.ts
CHANGED
|
@@ -4,8 +4,16 @@ export declare const FavoriteProductSchema: z.ZodObject<{
|
|
|
4
4
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
5
5
|
name: z.ZodString;
|
|
6
6
|
price: z.ZodNullable<z.ZodObject<{
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
amount: z.ZodObject<{
|
|
8
|
+
currency: z.ZodLiteral<"EUR">;
|
|
9
|
+
minorUnits: z.ZodNumber;
|
|
10
|
+
}, z.core.$strict>;
|
|
11
|
+
per: z.ZodEnum<{
|
|
12
|
+
item: "item";
|
|
13
|
+
kg: "kg";
|
|
14
|
+
l: "l";
|
|
15
|
+
piece: "piece";
|
|
16
|
+
}>;
|
|
9
17
|
}, z.core.$strict>>;
|
|
10
18
|
url: z.ZodURL;
|
|
11
19
|
}, z.core.$strict>;
|
|
@@ -15,11 +23,43 @@ export declare const ProductSchema: z.ZodObject<{
|
|
|
15
23
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
16
24
|
name: z.ZodString;
|
|
17
25
|
price: z.ZodNullable<z.ZodObject<{
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
amount: z.ZodObject<{
|
|
27
|
+
currency: z.ZodLiteral<"EUR">;
|
|
28
|
+
minorUnits: z.ZodNumber;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
per: z.ZodEnum<{
|
|
31
|
+
item: "item";
|
|
32
|
+
kg: "kg";
|
|
33
|
+
l: "l";
|
|
34
|
+
piece: "piece";
|
|
35
|
+
}>;
|
|
20
36
|
}, z.core.$strict>>;
|
|
21
37
|
url: z.ZodURL;
|
|
22
|
-
|
|
38
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
39
|
+
amount: z.ZodObject<{
|
|
40
|
+
currency: z.ZodLiteral<"EUR">;
|
|
41
|
+
minorUnits: z.ZodNumber;
|
|
42
|
+
}, z.core.$strict>;
|
|
43
|
+
per: z.ZodEnum<{
|
|
44
|
+
item: "item";
|
|
45
|
+
kg: "kg";
|
|
46
|
+
l: "l";
|
|
47
|
+
piece: "piece";
|
|
48
|
+
}>;
|
|
49
|
+
}, z.core.$strict>>;
|
|
50
|
+
packageText: z.ZodNullable<z.ZodString>;
|
|
51
|
+
recommendedPrice: z.ZodNullable<z.ZodObject<{
|
|
52
|
+
amount: z.ZodObject<{
|
|
53
|
+
currency: z.ZodLiteral<"EUR">;
|
|
54
|
+
minorUnits: z.ZodNumber;
|
|
55
|
+
}, z.core.$strict>;
|
|
56
|
+
per: z.ZodEnum<{
|
|
57
|
+
item: "item";
|
|
58
|
+
kg: "kg";
|
|
59
|
+
l: "l";
|
|
60
|
+
piece: "piece";
|
|
61
|
+
}>;
|
|
62
|
+
}, z.core.$strict>>;
|
|
23
63
|
minimumText: z.ZodNullable<z.ZodString>;
|
|
24
64
|
}, z.core.$strict>;
|
|
25
65
|
export type Product = z.infer<typeof ProductSchema>;
|
|
@@ -27,6 +67,10 @@ export declare const CartItemSchema: z.ZodObject<{
|
|
|
27
67
|
productId: z.core.$ZodBranded<z.ZodString, "ProductId", "out">;
|
|
28
68
|
cartItemId: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "CartItemId", "out">>;
|
|
29
69
|
name: z.ZodString;
|
|
70
|
+
kind: z.ZodEnum<{
|
|
71
|
+
bag: "bag";
|
|
72
|
+
product: "product";
|
|
73
|
+
}>;
|
|
30
74
|
quantity: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
31
75
|
status: z.ZodLiteral<"known">;
|
|
32
76
|
quantity: z.ZodObject<{
|
|
@@ -50,25 +94,62 @@ export declare const CartItemSchema: z.ZodObject<{
|
|
|
50
94
|
unsupported_unit: "unsupported_unit";
|
|
51
95
|
}>>;
|
|
52
96
|
}, z.core.$strict>], "status">;
|
|
53
|
-
price: z.ZodObject<{
|
|
54
|
-
amount: z.
|
|
97
|
+
price: z.ZodNullable<z.ZodObject<{
|
|
98
|
+
amount: z.ZodObject<{
|
|
55
99
|
currency: z.ZodLiteral<"EUR">;
|
|
56
100
|
minorUnits: z.ZodNumber;
|
|
57
|
-
}, z.core.$strict
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
101
|
+
}, z.core.$strict>;
|
|
102
|
+
per: z.ZodEnum<{
|
|
103
|
+
item: "item";
|
|
104
|
+
kg: "kg";
|
|
105
|
+
l: "l";
|
|
106
|
+
piece: "piece";
|
|
62
107
|
}>;
|
|
63
|
-
}, z.core.$strict
|
|
108
|
+
}, z.core.$strict>>;
|
|
109
|
+
lineTotal: z.ZodNullable<z.ZodObject<{
|
|
110
|
+
currency: z.ZodLiteral<"EUR">;
|
|
111
|
+
minorUnits: z.ZodNumber;
|
|
112
|
+
}, z.core.$strict>>;
|
|
64
113
|
}, z.core.$strict>;
|
|
65
114
|
export type CartItem = z.infer<typeof CartItemSchema>;
|
|
115
|
+
/** Each amount as the website reports it, or null when it reports none. */
|
|
116
|
+
export declare const CartTotalsSchema: z.ZodObject<{
|
|
117
|
+
products: z.ZodNullable<z.ZodObject<{
|
|
118
|
+
currency: z.ZodLiteral<"EUR">;
|
|
119
|
+
minorUnits: z.ZodNumber;
|
|
120
|
+
}, z.core.$strict>>;
|
|
121
|
+
bags: z.ZodNullable<z.ZodObject<{
|
|
122
|
+
currency: z.ZodLiteral<"EUR">;
|
|
123
|
+
minorUnits: z.ZodNumber;
|
|
124
|
+
}, z.core.$strict>>;
|
|
125
|
+
deposit: z.ZodNullable<z.ZodObject<{
|
|
126
|
+
currency: z.ZodLiteral<"EUR">;
|
|
127
|
+
minorUnits: z.ZodNumber;
|
|
128
|
+
}, z.core.$strict>>;
|
|
129
|
+
delivery: z.ZodNullable<z.ZodObject<{
|
|
130
|
+
currency: z.ZodLiteral<"EUR">;
|
|
131
|
+
minorUnits: z.ZodNumber;
|
|
132
|
+
}, z.core.$strict>>;
|
|
133
|
+
discounts: z.ZodNullable<z.ZodObject<{
|
|
134
|
+
currency: z.ZodLiteral<"EUR">;
|
|
135
|
+
minorUnits: z.ZodNumber;
|
|
136
|
+
}, z.core.$strict>>;
|
|
137
|
+
grandTotal: z.ZodNullable<z.ZodObject<{
|
|
138
|
+
currency: z.ZodLiteral<"EUR">;
|
|
139
|
+
minorUnits: z.ZodNumber;
|
|
140
|
+
}, z.core.$strict>>;
|
|
141
|
+
}, z.core.$strict>;
|
|
142
|
+
export type CartTotals = z.infer<typeof CartTotalsSchema>;
|
|
66
143
|
export declare const CartSchema: z.ZodObject<{
|
|
67
144
|
customerAuthenticated: z.ZodBoolean;
|
|
68
145
|
items: z.ZodArray<z.ZodObject<{
|
|
69
146
|
productId: z.core.$ZodBranded<z.ZodString, "ProductId", "out">;
|
|
70
147
|
cartItemId: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "CartItemId", "out">>;
|
|
71
148
|
name: z.ZodString;
|
|
149
|
+
kind: z.ZodEnum<{
|
|
150
|
+
bag: "bag";
|
|
151
|
+
product: "product";
|
|
152
|
+
}>;
|
|
72
153
|
quantity: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
73
154
|
status: z.ZodLiteral<"known">;
|
|
74
155
|
quantity: z.ZodObject<{
|
|
@@ -92,22 +173,49 @@ export declare const CartSchema: z.ZodObject<{
|
|
|
92
173
|
unsupported_unit: "unsupported_unit";
|
|
93
174
|
}>>;
|
|
94
175
|
}, z.core.$strict>], "status">;
|
|
95
|
-
price: z.ZodObject<{
|
|
96
|
-
amount: z.
|
|
176
|
+
price: z.ZodNullable<z.ZodObject<{
|
|
177
|
+
amount: z.ZodObject<{
|
|
97
178
|
currency: z.ZodLiteral<"EUR">;
|
|
98
179
|
minorUnits: z.ZodNumber;
|
|
99
|
-
}, z.core.$strict
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
180
|
+
}, z.core.$strict>;
|
|
181
|
+
per: z.ZodEnum<{
|
|
182
|
+
item: "item";
|
|
183
|
+
kg: "kg";
|
|
184
|
+
l: "l";
|
|
185
|
+
piece: "piece";
|
|
104
186
|
}>;
|
|
105
|
-
}, z.core.$strict
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
187
|
+
}, z.core.$strict>>;
|
|
188
|
+
lineTotal: z.ZodNullable<z.ZodObject<{
|
|
189
|
+
currency: z.ZodLiteral<"EUR">;
|
|
190
|
+
minorUnits: z.ZodNumber;
|
|
191
|
+
}, z.core.$strict>>;
|
|
110
192
|
}, z.core.$strict>>;
|
|
193
|
+
totals: z.ZodObject<{
|
|
194
|
+
products: z.ZodNullable<z.ZodObject<{
|
|
195
|
+
currency: z.ZodLiteral<"EUR">;
|
|
196
|
+
minorUnits: z.ZodNumber;
|
|
197
|
+
}, z.core.$strict>>;
|
|
198
|
+
bags: z.ZodNullable<z.ZodObject<{
|
|
199
|
+
currency: z.ZodLiteral<"EUR">;
|
|
200
|
+
minorUnits: z.ZodNumber;
|
|
201
|
+
}, z.core.$strict>>;
|
|
202
|
+
deposit: z.ZodNullable<z.ZodObject<{
|
|
203
|
+
currency: z.ZodLiteral<"EUR">;
|
|
204
|
+
minorUnits: z.ZodNumber;
|
|
205
|
+
}, z.core.$strict>>;
|
|
206
|
+
delivery: z.ZodNullable<z.ZodObject<{
|
|
207
|
+
currency: z.ZodLiteral<"EUR">;
|
|
208
|
+
minorUnits: z.ZodNumber;
|
|
209
|
+
}, z.core.$strict>>;
|
|
210
|
+
discounts: z.ZodNullable<z.ZodObject<{
|
|
211
|
+
currency: z.ZodLiteral<"EUR">;
|
|
212
|
+
minorUnits: z.ZodNumber;
|
|
213
|
+
}, z.core.$strict>>;
|
|
214
|
+
grandTotal: z.ZodNullable<z.ZodObject<{
|
|
215
|
+
currency: z.ZodLiteral<"EUR">;
|
|
216
|
+
minorUnits: z.ZodNumber;
|
|
217
|
+
}, z.core.$strict>>;
|
|
218
|
+
}, z.core.$strict>;
|
|
111
219
|
}, z.core.$strict>;
|
|
112
220
|
export type Cart = z.infer<typeof CartSchema>;
|
|
113
221
|
export declare const OrderLineSchema: z.ZodObject<{
|
|
@@ -227,7 +335,7 @@ export declare const PurchaseFrequencySchema: z.ZodObject<{
|
|
|
227
335
|
}, z.core.$strict>;
|
|
228
336
|
export type PurchaseFrequency = z.infer<typeof PurchaseFrequencySchema>;
|
|
229
337
|
export declare const FavoritesCacheSchema: z.ZodObject<{
|
|
230
|
-
schemaVersion: z.ZodLiteral<
|
|
338
|
+
schemaVersion: z.ZodLiteral<2>;
|
|
231
339
|
accountScope: z.ZodString;
|
|
232
340
|
fetchedAt: z.ZodISODateTime;
|
|
233
341
|
favorites: z.ZodArray<z.ZodObject<{
|
|
@@ -235,8 +343,16 @@ export declare const FavoritesCacheSchema: z.ZodObject<{
|
|
|
235
343
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
236
344
|
name: z.ZodString;
|
|
237
345
|
price: z.ZodNullable<z.ZodObject<{
|
|
238
|
-
|
|
239
|
-
|
|
346
|
+
amount: z.ZodObject<{
|
|
347
|
+
currency: z.ZodLiteral<"EUR">;
|
|
348
|
+
minorUnits: z.ZodNumber;
|
|
349
|
+
}, z.core.$strict>;
|
|
350
|
+
per: z.ZodEnum<{
|
|
351
|
+
item: "item";
|
|
352
|
+
kg: "kg";
|
|
353
|
+
l: "l";
|
|
354
|
+
piece: "piece";
|
|
355
|
+
}>;
|
|
240
356
|
}, z.core.$strict>>;
|
|
241
357
|
url: z.ZodURL;
|
|
242
358
|
}, z.core.$strict>>;
|
|
@@ -248,18 +364,42 @@ export declare const ProductDetailsSchema: z.ZodObject<{
|
|
|
248
364
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
249
365
|
name: z.ZodString;
|
|
250
366
|
url: z.ZodURL;
|
|
251
|
-
price: z.ZodObject<{
|
|
252
|
-
amount: z.
|
|
367
|
+
price: z.ZodNullable<z.ZodObject<{
|
|
368
|
+
amount: z.ZodObject<{
|
|
253
369
|
currency: z.ZodLiteral<"EUR">;
|
|
254
370
|
minorUnits: z.ZodNumber;
|
|
255
|
-
}, z.core.$strict
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
371
|
+
}, z.core.$strict>;
|
|
372
|
+
per: z.ZodEnum<{
|
|
373
|
+
item: "item";
|
|
374
|
+
kg: "kg";
|
|
375
|
+
l: "l";
|
|
376
|
+
piece: "piece";
|
|
260
377
|
}>;
|
|
261
|
-
}, z.core.$strict
|
|
262
|
-
|
|
378
|
+
}, z.core.$strict>>;
|
|
379
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
380
|
+
amount: z.ZodObject<{
|
|
381
|
+
currency: z.ZodLiteral<"EUR">;
|
|
382
|
+
minorUnits: z.ZodNumber;
|
|
383
|
+
}, z.core.$strict>;
|
|
384
|
+
per: z.ZodEnum<{
|
|
385
|
+
item: "item";
|
|
386
|
+
kg: "kg";
|
|
387
|
+
l: "l";
|
|
388
|
+
piece: "piece";
|
|
389
|
+
}>;
|
|
390
|
+
}, z.core.$strict>>;
|
|
391
|
+
recommendedPrice: z.ZodNullable<z.ZodObject<{
|
|
392
|
+
amount: z.ZodObject<{
|
|
393
|
+
currency: z.ZodLiteral<"EUR">;
|
|
394
|
+
minorUnits: z.ZodNumber;
|
|
395
|
+
}, z.core.$strict>;
|
|
396
|
+
per: z.ZodEnum<{
|
|
397
|
+
item: "item";
|
|
398
|
+
kg: "kg";
|
|
399
|
+
l: "l";
|
|
400
|
+
piece: "piece";
|
|
401
|
+
}>;
|
|
402
|
+
}, z.core.$strict>>;
|
|
263
403
|
availability: z.ZodEnum<{
|
|
264
404
|
available: "available";
|
|
265
405
|
unavailable: "unavailable";
|
package/dist/domain/models.js
CHANGED
|
@@ -1,29 +1,54 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ToolErrorSchema } from './errors.js';
|
|
3
3
|
import { CartItemIdSchema, OrderIdSchema, OrderNumberSchema, ProductIdSchema, ProductReferenceSchema } from './identity.js';
|
|
4
|
-
import { MoneySchema,
|
|
4
|
+
import { MoneySchema, PriceTagSchema } from './money.js';
|
|
5
5
|
import { ObservedQuantitySchema, QuantitySchema, ProductSaleRulesSchema } from './quantity.js';
|
|
6
6
|
const productDetails = {
|
|
7
7
|
name: z.string().trim().min(1).max(250),
|
|
8
|
-
price
|
|
8
|
+
/** The main price as displayed; null when absent or unrecognised. */
|
|
9
|
+
price: PriceTagSchema.nullable(),
|
|
9
10
|
url: z.url(),
|
|
10
11
|
};
|
|
11
12
|
export const FavoriteProductSchema = ProductReferenceSchema.safeExtend(productDetails);
|
|
12
13
|
export const ProductSchema = FavoriteProductSchema.safeExtend({
|
|
13
|
-
|
|
14
|
+
/** The comparison price shown next to the main one, such as per kg or per piece. */
|
|
15
|
+
secondaryPrice: PriceTagSchema.nullable(),
|
|
16
|
+
/** Pack size as displayed, such as `940 gr`, `2 x 52 gr` or `1,2 kg (aprox.)`. */
|
|
17
|
+
packageText: z.string().nullable(),
|
|
18
|
+
/** The recommended retail price (PVPR) shown beside a promotional price; null when the website shows none. */
|
|
19
|
+
recommendedPrice: PriceTagSchema.nullable(),
|
|
14
20
|
minimumText: z.string().nullable(),
|
|
15
21
|
});
|
|
16
22
|
export const CartItemSchema = z.strictObject({
|
|
17
23
|
productId: ProductIdSchema,
|
|
18
24
|
cartItemId: CartItemIdSchema.nullable(),
|
|
19
25
|
name: z.string().trim().min(1),
|
|
26
|
+
/** `bag` for the shopping bags the website adds to the cart on its own; `product` for everything else. */
|
|
27
|
+
kind: z.enum(['product', 'bag']),
|
|
20
28
|
quantity: ObservedQuantitySchema,
|
|
21
|
-
price:
|
|
29
|
+
/** Unit price with what it is for: per item, or per kg for products sold by the piece but priced by weight. */
|
|
30
|
+
price: PriceTagSchema.nullable(),
|
|
31
|
+
/** The line's total as the website reports it. */
|
|
32
|
+
lineTotal: MoneySchema.nullable(),
|
|
33
|
+
});
|
|
34
|
+
/** Each amount as the website reports it, or null when it reports none. */
|
|
35
|
+
export const CartTotalsSchema = z.strictObject({
|
|
36
|
+
/** Product lines only, without bags or deposit. */
|
|
37
|
+
products: MoneySchema.nullable(),
|
|
38
|
+
/** The bag lines. */
|
|
39
|
+
bags: MoneySchema.nullable(),
|
|
40
|
+
/** Bottle and can deposit (SDR). */
|
|
41
|
+
deposit: MoneySchema.nullable(),
|
|
42
|
+
delivery: MoneySchema.nullable(),
|
|
43
|
+
/** Amount taken off by order and delivery discounts. */
|
|
44
|
+
discounts: MoneySchema.nullable(),
|
|
45
|
+
/** The amount to pay as the website shows it; it includes the bags. */
|
|
46
|
+
grandTotal: MoneySchema.nullable(),
|
|
22
47
|
});
|
|
23
48
|
export const CartSchema = z.strictObject({
|
|
24
49
|
customerAuthenticated: z.boolean(),
|
|
25
50
|
items: z.array(CartItemSchema),
|
|
26
|
-
|
|
51
|
+
totals: CartTotalsSchema,
|
|
27
52
|
});
|
|
28
53
|
export const OrderLineSchema = z.strictObject({
|
|
29
54
|
productId: ProductIdSchema.nullable(),
|
|
@@ -51,15 +76,15 @@ export const PurchaseFrequencySchema = z.strictObject({
|
|
|
51
76
|
}).refine(frequency => (frequency.identityBasis === 'product') === (frequency.productId !== null), { message: 'Identity basis must agree with the available product ID' })
|
|
52
77
|
.refine(frequency => new Set(frequency.quantities.map(quantity => quantity.unit)).size === frequency.quantities.length, { message: 'Purchase totals must contain each unit only once' });
|
|
53
78
|
export const FavoritesCacheSchema = z.strictObject({
|
|
54
|
-
schemaVersion: z.literal(
|
|
79
|
+
schemaVersion: z.literal(2),
|
|
55
80
|
accountScope: z.string().trim().min(1).max(128),
|
|
56
81
|
fetchedAt: z.iso.datetime({ offset: true }),
|
|
57
82
|
favorites: z.array(FavoriteProductSchema),
|
|
58
83
|
completeness: z.literal('complete').optional(),
|
|
59
84
|
});
|
|
60
85
|
export const ProductDetailsSchema = ProductReferenceSchema.safeExtend({
|
|
61
|
-
name: productDetails.name, url: productDetails.url, price:
|
|
62
|
-
|
|
86
|
+
name: productDetails.name, url: productDetails.url, price: PriceTagSchema.nullable(),
|
|
87
|
+
secondaryPrice: PriceTagSchema.nullable(), recommendedPrice: PriceTagSchema.nullable(), availability: z.enum(['available', 'unavailable', 'unknown']),
|
|
63
88
|
saleRules: z.discriminatedUnion('status', [
|
|
64
89
|
z.strictObject({ status: z.literal('available'), rules: ProductSaleRulesSchema }),
|
|
65
90
|
z.strictObject({ status: z.literal('unavailable'), error: ToolErrorSchema }),
|
package/dist/domain/money.d.ts
CHANGED
|
@@ -6,18 +6,24 @@ export declare const MoneySchema: z.ZodObject<{
|
|
|
6
6
|
minorUnits: z.ZodNumber;
|
|
7
7
|
}, z.core.$strict>;
|
|
8
8
|
export type Money = z.infer<typeof MoneySchema>;
|
|
9
|
-
/**
|
|
10
|
-
export declare
|
|
11
|
-
|
|
9
|
+
/** Convert decimal text through integer arithmetic; never round an unrecognized precision. */
|
|
10
|
+
export declare function parseMoneyAmount(input: unknown): Result<Money | null, ToolError>;
|
|
11
|
+
/**
|
|
12
|
+
* What a displayed price is for. The storefront shows packaged goods per `item` (one pack or
|
|
13
|
+
* article), produce and meat per `kg`, drinks per `l`, and an approximate price per `piece`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PriceTagSchema: z.ZodObject<{
|
|
16
|
+
amount: z.ZodObject<{
|
|
12
17
|
currency: z.ZodLiteral<"EUR">;
|
|
13
18
|
minorUnits: z.ZodNumber;
|
|
14
|
-
}, z.core.$strict
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
}, z.core.$strict>;
|
|
20
|
+
per: z.ZodEnum<{
|
|
21
|
+
item: "item";
|
|
22
|
+
kg: "kg";
|
|
23
|
+
l: "l";
|
|
24
|
+
piece: "piece";
|
|
19
25
|
}>;
|
|
20
26
|
}, z.core.$strict>;
|
|
21
|
-
export type
|
|
22
|
-
/**
|
|
23
|
-
export declare function
|
|
27
|
+
export type PriceTag = z.infer<typeof PriceTagSchema>;
|
|
28
|
+
/** Parse labels such as `1,29€`, `1,19€/kg` or `0,24€/un`. An empty label is an unknown price. */
|
|
29
|
+
export declare function parsePriceTag(input: unknown): Result<PriceTag | null, ToolError>;
|
package/dist/domain/money.js
CHANGED
|
@@ -4,11 +4,6 @@ export const MoneySchema = z.strictObject({
|
|
|
4
4
|
currency: z.literal('EUR'),
|
|
5
5
|
minorUnits: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
6
6
|
});
|
|
7
|
-
/** The source of a price determines its basis; a line total is not a unit price. */
|
|
8
|
-
export const QuotedMoneySchema = z.strictObject({
|
|
9
|
-
amount: MoneySchema.nullable(),
|
|
10
|
-
basis: z.enum(['unit', 'line', 'unknown']),
|
|
11
|
-
});
|
|
12
7
|
/** Convert decimal text through integer arithmetic; never round an unrecognized precision. */
|
|
13
8
|
export function parseMoneyAmount(input) {
|
|
14
9
|
if (input === null || input === undefined || input === '')
|
|
@@ -28,3 +23,28 @@ export function parseMoneyAmount(input) {
|
|
|
28
23
|
}
|
|
29
24
|
return { ok: true, value: { currency: 'EUR', minorUnits: Number(minorUnits) } };
|
|
30
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* What a displayed price is for. The storefront shows packaged goods per `item` (one pack or
|
|
28
|
+
* article), produce and meat per `kg`, drinks per `l`, and an approximate price per `piece`.
|
|
29
|
+
*/
|
|
30
|
+
export const PriceTagSchema = z.strictObject({
|
|
31
|
+
amount: MoneySchema,
|
|
32
|
+
per: z.enum(['item', 'piece', 'kg', 'l']),
|
|
33
|
+
});
|
|
34
|
+
const PRICE_UNITS = { kg: 'kg', l: 'l', lt: 'l', un: 'piece' };
|
|
35
|
+
/** Parse labels such as `1,29€`, `1,19€/kg` or `0,24€/un`. An empty label is an unknown price. */
|
|
36
|
+
export function parsePriceTag(input) {
|
|
37
|
+
if (input === null || input === undefined)
|
|
38
|
+
return { ok: true, value: null };
|
|
39
|
+
if (typeof input !== 'string')
|
|
40
|
+
return { ok: false, error: invalidData('price') };
|
|
41
|
+
const text = input.replace(/\s+/g, '');
|
|
42
|
+
if (!text)
|
|
43
|
+
return { ok: true, value: null };
|
|
44
|
+
const match = /^(\d+(?:[.,]\d{1,2})?)€(?:\/([a-z]+))?$/i.exec(text);
|
|
45
|
+
const per = match ? PRICE_UNITS[(match[2] ?? '').toLowerCase()] ?? (match[2] === undefined ? 'item' : undefined) : undefined;
|
|
46
|
+
const amount = match && per ? parseMoneyAmount(match[1]) : null;
|
|
47
|
+
if (!amount?.ok || !amount.value || !per)
|
|
48
|
+
return { ok: false, error: { ...invalidData('price'), message: 'Unrecognised price format' } };
|
|
49
|
+
return { ok: true, value: { amount: amount.value, per } };
|
|
50
|
+
}
|