albert-heijn-mcp 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +376 -0
  3. package/assets/icon.png +0 -0
  4. package/deploy/albert-heijn-mcp.service +42 -0
  5. package/dist/ahapi/auth.js +18 -0
  6. package/dist/ahapi/auth.js.map +1 -0
  7. package/dist/ahapi/bonus.js +53 -0
  8. package/dist/ahapi/bonus.js.map +1 -0
  9. package/dist/ahapi/client.js +115 -0
  10. package/dist/ahapi/client.js.map +1 -0
  11. package/dist/ahapi/favorites.js +67 -0
  12. package/dist/ahapi/favorites.js.map +1 -0
  13. package/dist/ahapi/index.js +12 -0
  14. package/dist/ahapi/index.js.map +1 -0
  15. package/dist/ahapi/member.js +23 -0
  16. package/dist/ahapi/member.js.map +1 -0
  17. package/dist/ahapi/orders.js +85 -0
  18. package/dist/ahapi/orders.js.map +1 -0
  19. package/dist/ahapi/products.js +103 -0
  20. package/dist/ahapi/products.js.map +1 -0
  21. package/dist/ahapi/receipts.js +37 -0
  22. package/dist/ahapi/receipts.js.map +1 -0
  23. package/dist/ahapi/recipes.js +57 -0
  24. package/dist/ahapi/recipes.js.map +1 -0
  25. package/dist/ahapi/shoppingList.js +58 -0
  26. package/dist/ahapi/shoppingList.js.map +1 -0
  27. package/dist/ahapi/stores.js +35 -0
  28. package/dist/ahapi/stores.js.map +1 -0
  29. package/dist/auth/login.js +15 -0
  30. package/dist/auth/login.js.map +1 -0
  31. package/dist/auth/session.js +125 -0
  32. package/dist/auth/session.js.map +1 -0
  33. package/dist/auth/tokens.js +132 -0
  34. package/dist/auth/tokens.js.map +1 -0
  35. package/dist/config.js +61 -0
  36. package/dist/config.js.map +1 -0
  37. package/dist/index.js +70 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/log.js +70 -0
  40. package/dist/log.js.map +1 -0
  41. package/dist/server/http.js +99 -0
  42. package/dist/server/http.js.map +1 -0
  43. package/dist/tools/account.js +126 -0
  44. package/dist/tools/account.js.map +1 -0
  45. package/dist/tools/bonus.js +151 -0
  46. package/dist/tools/bonus.js.map +1 -0
  47. package/dist/tools/cache.js +44 -0
  48. package/dist/tools/cache.js.map +1 -0
  49. package/dist/tools/cart.js +127 -0
  50. package/dist/tools/cart.js.map +1 -0
  51. package/dist/tools/common.js +173 -0
  52. package/dist/tools/common.js.map +1 -0
  53. package/dist/tools/favorites.js +103 -0
  54. package/dist/tools/favorites.js.map +1 -0
  55. package/dist/tools/index.js +24 -0
  56. package/dist/tools/index.js.map +1 -0
  57. package/dist/tools/orders.js +141 -0
  58. package/dist/tools/orders.js.map +1 -0
  59. package/dist/tools/products.js +165 -0
  60. package/dist/tools/products.js.map +1 -0
  61. package/dist/tools/receipts.js +52 -0
  62. package/dist/tools/receipts.js.map +1 -0
  63. package/dist/tools/recipes.js +261 -0
  64. package/dist/tools/recipes.js.map +1 -0
  65. package/dist/tools/shoppingList.js +149 -0
  66. package/dist/tools/shoppingList.js.map +1 -0
  67. package/dist/tools/stores.js +108 -0
  68. package/dist/tools/stores.js.map +1 -0
  69. package/dist/tools/views.js +100 -0
  70. package/dist/tools/views.js.map +1 -0
  71. package/package.json +53 -0
@@ -0,0 +1,53 @@
1
+ import { fromGraphqlProduct, GRAPHQL_PRODUCT_FIELDS } from "./products.js";
2
+ /** Published bonus periods, current week first. */
3
+ export async function getBonusPeriods(c) {
4
+ const res = await c.request("/mobile-services/bonuspage/v3/metadata");
5
+ return (res.periods ?? []).map((p) => {
6
+ const categories = new Set();
7
+ for (const tab of p.tabs ?? []) {
8
+ for (const m of tab.urlMetadataList ?? []) {
9
+ if (m.bonusType === "NATIONAL" && m.description)
10
+ categories.add(m.description);
11
+ }
12
+ }
13
+ return {
14
+ start: p.bonusStartDate,
15
+ end: p.bonusEndDate,
16
+ nextVisibleFrom: p.nextPeriodVisibleFrom,
17
+ categories: [...categories],
18
+ };
19
+ });
20
+ }
21
+ /** The ah.nl page of a group deal, listing its products. */
22
+ export function bonusGroupUrl(segmentId) {
23
+ return `https://www.ah.nl/bonus/groep/${segmentId}`;
24
+ }
25
+ async function fetchSection(c, path, params) {
26
+ const query = new URLSearchParams({ application: "AHWEBSHOP", ...params });
27
+ const res = await c.request(`/mobile-services/bonuspage/v2/${path}?${query}`);
28
+ return (res.bonusGroupOrProducts ?? []).map((it) => ({ product: it.product, group: it.bonusGroup }));
29
+ }
30
+ /** National offers of a category; date picks the bonus period. */
31
+ export function getBonusSection(c, category, date) {
32
+ return fetchSection(c, "section", { date, promotionType: "NATIONAL", category });
33
+ }
34
+ /** Offers on products the member bought before (AH's "Eerder gekocht"); date picks the bonus period. */
35
+ export function getPreviouslyBoughtBonus(c, date) {
36
+ return fetchSection(c, "section/previously-bought", { date });
37
+ }
38
+ /** Products of a group deal. */
39
+ export async function getBonusGroupProducts(c, groupId, period) {
40
+ const query = `query BonusGroupProducts($id: String, $start: String, $end: String) {
41
+ bonusPromotions(input: {id: $id, periodStart: $start, periodEnd: $end}) {
42
+ products { ${GRAPHQL_PRODUCT_FIELDS} }
43
+ }
44
+ }`;
45
+ const data = await c.graphql(query, {
46
+ id: groupId,
47
+ start: period.start,
48
+ end: period.end,
49
+ });
50
+ // Every product in a bonus group is on bonus, even when the price doesn't show it (e.g. "2+1 gratis").
51
+ return data.bonusPromotions.flatMap((promo) => promo.products.map((p) => ({ ...fromGraphqlProduct(p), isBonus: true })));
52
+ }
53
+ //# sourceMappingURL=bonus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bonus.js","sourceRoot":"","sources":["../../src/ahapi/bonus.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ // Client for the AH mobile app API (REST under /mobile-services, plus GraphQL).
2
+ import { describeError } from "../log.js";
3
+ // The API expects the official iOS app. If AH starts rejecting requests,
4
+ // update the version and user agent to the current app release first.
5
+ export const CLIENT_NAME = "appie-ios";
6
+ const CLIENT_VERSION = "9.28";
7
+ const USER_AGENT = "Appie/9.28 (iPhone17,3; iPhone; CPU OS 26_1 like Mac OS X)";
8
+ const BASE_URL = "https://api.ah.nl";
9
+ const TIMEOUT_MS = 30_000;
10
+ /** A non-2xx API response. */
11
+ export class AhApiError extends Error {
12
+ status;
13
+ /** body usually explains the error. */
14
+ constructor(status, body) {
15
+ super(`AH API error ${status}: ${body}`);
16
+ this.name = "AhApiError";
17
+ this.status = status;
18
+ }
19
+ logText() {
20
+ return `AH API error ${this.status}`;
21
+ }
22
+ }
23
+ /** Reports whether err is an AhApiError with this HTTP status. */
24
+ export function hasStatus(err, status) {
25
+ return err instanceof AhApiError && err.status === status;
26
+ }
27
+ /** An error with our own context in front of its cause's message. */
28
+ export class ContextError extends Error {
29
+ /** Our text, without the cause's (which may quote AH). */
30
+ context;
31
+ constructor(context, cause) {
32
+ super(`${context}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
33
+ this.name = "ContextError";
34
+ this.context = context;
35
+ }
36
+ logText() {
37
+ return `${this.context}: ${describeError(this.cause)}`;
38
+ }
39
+ }
40
+ /** Wraps err with context, keeping it as the cause so its HTTP status stays visible. */
41
+ export function wrapError(message, err) {
42
+ return new ContextError(message, err);
43
+ }
44
+ /** The errors array of a GraphQL response. */
45
+ export class GraphQLError extends Error {
46
+ constructor(messages) {
47
+ super(`AH GraphQL error: ${messages.join("; ")}`);
48
+ this.name = "GraphQLError";
49
+ }
50
+ logText() {
51
+ return "AH GraphQL error";
52
+ }
53
+ }
54
+ export class AhClient {
55
+ /** Access token sent as a Bearer token with every request, if any. */
56
+ token;
57
+ constructor(token) {
58
+ this.token = token;
59
+ }
60
+ /** Sends a REST request; returns the parsed JSON body, or undefined if empty. */
61
+ async request(path, options = {}) {
62
+ const headers = {
63
+ "User-Agent": USER_AGENT,
64
+ "x-client-name": CLIENT_NAME,
65
+ "x-client-version": CLIENT_VERSION,
66
+ "x-application": "AHWEBSHOP",
67
+ Accept: "application/json",
68
+ };
69
+ if (options.body !== undefined)
70
+ headers["Content-Type"] = "application/json";
71
+ if (this.token)
72
+ headers.Authorization = `Bearer ${this.token}`;
73
+ if (options.orderId)
74
+ headers["Appie-Current-Order-Id"] = String(options.orderId);
75
+ const res = await fetch(BASE_URL + path, {
76
+ method: options.method ?? "GET",
77
+ headers,
78
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
79
+ signal: AbortSignal.timeout(TIMEOUT_MS),
80
+ });
81
+ const text = await res.text();
82
+ if (!res.ok)
83
+ throw new AhApiError(res.status, text.trim());
84
+ return (text ? JSON.parse(text) : undefined);
85
+ }
86
+ /** Runs a GraphQL operation; returns its data or throws GraphQLError. */
87
+ async graphql(query, variables) {
88
+ const res = await this.request("/graphql", {
89
+ method: "POST",
90
+ body: { query, variables: variables ?? {} },
91
+ });
92
+ if (res.errors?.length)
93
+ throw new GraphQLError(res.errors.map((e) => e.message));
94
+ return res.data;
95
+ }
96
+ }
97
+ /** A mutation AH answered with a status other than SUCCESS. */
98
+ export class MutationError extends Error {
99
+ status;
100
+ constructor(result) {
101
+ const status = result?.status ?? "no result";
102
+ super(`mutation failed (${status}): ${result?.errorMessage ?? ""}`);
103
+ this.name = "MutationError";
104
+ this.status = status;
105
+ }
106
+ logText() {
107
+ return `mutation failed (${this.status})`;
108
+ }
109
+ }
110
+ /** Throws unless status is SUCCESS. */
111
+ export function checkMutation(result) {
112
+ if (result?.status !== "SUCCESS")
113
+ throw new MutationError(result);
114
+ }
115
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/ahapi/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import { checkMutation } from "./client.js";
2
+ function toFavoriteList(l) {
3
+ return { id: l.id, name: l.description, itemCount: l.totalSize, updatedAt: l.updatedAt };
4
+ }
5
+ /** The lists with these ids; all lists if ids is empty. */
6
+ export async function getFavoriteLists(c, ids = []) {
7
+ const query = `query FavoriteLists($ids: [String!]!) {
8
+ favoriteListV2(ids: $ids) { id description totalSize updatedAt }
9
+ }`;
10
+ const data = await c.graphql(query, { ids });
11
+ return (data.favoriteListV2 ?? []).map(toFavoriteList);
12
+ }
13
+ /** Adds products (product ID → quantity), or updates quantities of ones already listed. */
14
+ export async function addToFavoriteList(c, listId, quantities) {
15
+ const mutation = `mutation AddToFavoriteList($id: String!, $products: [FavoriteListProductMutation!]!) {
16
+ favoriteListProductsAddV2(id: $id, products: $products) { status errorMessage }
17
+ }`;
18
+ const products = [...quantities].map(([productId, quantity]) => ({ productId, quantity }));
19
+ const data = await c.graphql(mutation, { id: listId, products });
20
+ checkMutation(data.favoriteListProductsAddV2);
21
+ }
22
+ /** Removes products; returns how many were on the list. */
23
+ export async function removeFromFavoriteList(c, listId, productIds) {
24
+ // The delete mutation takes item IDs, not product IDs.
25
+ const query = `query FavoriteListItems($ids: [String!]!) {
26
+ favoriteListV2(ids: $ids) { items { id productId } }
27
+ }`;
28
+ const lists = await c.graphql(query, {
29
+ ids: [listId],
30
+ });
31
+ const list = lists.favoriteListV2?.[0];
32
+ if (!list)
33
+ throw new Error(`favorite list ${listId} not found`);
34
+ const remove = new Set(productIds);
35
+ const itemIds = list.items.filter((it) => remove.has(it.productId)).map((it) => it.id);
36
+ if (itemIds.length === 0)
37
+ throw new Error("none of these products are on the favorite list");
38
+ const mutation = `mutation RemoveFromFavoriteList($id: String!, $itemIds: [String!]!) {
39
+ favoriteListProductsDeleteV2(id: $id, itemIds: $itemIds) { status errorMessage }
40
+ }`;
41
+ const data = await c.graphql(mutation, { id: listId, itemIds });
42
+ checkMutation(data.favoriteListProductsDeleteV2);
43
+ return itemIds.length;
44
+ }
45
+ /** Creates an empty favourite list. */
46
+ export async function createFavoriteList(c, name) {
47
+ const mutation = `mutation CreateFavoriteList($name: String!) {
48
+ favoriteListAddV2(description: $name) { status errorMessage result { id description totalSize } }
49
+ }`;
50
+ const data = await c.graphql(mutation, {
51
+ name,
52
+ });
53
+ checkMutation(data.favoriteListAddV2);
54
+ const l = data.favoriteListAddV2.result;
55
+ if (!l)
56
+ throw new Error("AH created the list but returned no list");
57
+ return toFavoriteList(l);
58
+ }
59
+ /** Deletes a favourite list and its items. */
60
+ export async function deleteFavoriteList(c, listId) {
61
+ const mutation = `mutation DeleteFavoriteList($id: String!) {
62
+ favoriteListDeleteV2(id: $id) { status errorMessage }
63
+ }`;
64
+ const data = await c.graphql(mutation, { id: listId });
65
+ checkMutation(data.favoriteListDeleteV2);
66
+ }
67
+ //# sourceMappingURL=favorites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"favorites.js","sourceRoot":"","sources":["../../src/ahapi/favorites.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export * from "./auth.js";
2
+ export * from "./bonus.js";
3
+ export * from "./client.js";
4
+ export * from "./favorites.js";
5
+ export * from "./member.js";
6
+ export * from "./orders.js";
7
+ export * from "./products.js";
8
+ export * from "./receipts.js";
9
+ export * from "./recipes.js";
10
+ export * from "./shoppingList.js";
11
+ export * from "./stores.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ahapi/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC"}
@@ -0,0 +1,23 @@
1
+ export async function getMember(c) {
2
+ const query = `query Member {
3
+ member {
4
+ name { first last }
5
+ emailAddress
6
+ address { street houseNumber houseNumberExtra postalCode city countryCode }
7
+ cards { bonus }
8
+ }
9
+ }`;
10
+ const { member: m } = await c.graphql(query);
11
+ return {
12
+ firstName: m.name?.first ?? "",
13
+ lastName: m.name?.last ?? "",
14
+ email: m.emailAddress ?? "",
15
+ postalCode: m.address?.postalCode ?? "",
16
+ bonusCardNumber: m.cards?.bonus ?? "",
17
+ address: isComplete(m.address) ? m.address : undefined,
18
+ };
19
+ }
20
+ function isComplete(a) {
21
+ return Boolean(a?.street && a.houseNumber && a.postalCode && a.city && a.countryCode);
22
+ }
23
+ //# sourceMappingURL=member.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"member.js","sourceRoot":"","sources":["../../src/ahapi/member.ts"],"names":[],"mappings":"AAsBA,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,CAAW;IACzC,MAAM,KAAK,GAAG;;;;;;;EAOd,CAAC;IACD,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,OAAO,CAOlC,KAAK,CAAC,CAAC;IACV,OAAO;QACL,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;QAC9B,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE;QAC5B,KAAK,EAAE,CAAC,CAAC,YAAY,IAAI,EAAE;QAC3B,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,IAAI,EAAE;QACvC,eAAe,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;QACrC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;KACvD,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,CAA4C;IAC9D,OAAO,OAAO,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,WAAW,CAAC,CAAC;AACxF,CAAC"}
@@ -0,0 +1,85 @@
1
+ function toItem(p) {
2
+ return {
3
+ productId: p.product.webshopId,
4
+ title: p.product.title,
5
+ quantity: p.quantity,
6
+ price: p.product.currentPrice || p.product.priceBeforeBonus,
7
+ };
8
+ }
9
+ /** Reports whether err means "no active delivery order". */
10
+ export function isNoActiveOrder(err) {
11
+ return err instanceof Error && err.message.includes("Order does not exist");
12
+ }
13
+ /** The order being filled before checkout; exists once a slot is chosen. Otherwise throws (see isNoActiveOrder). */
14
+ export async function getActiveOrder(c) {
15
+ const res = await c.request("/mobile-services/order/v1/summaries/active?sortBy=DEFAULT");
16
+ return {
17
+ id: res.id,
18
+ state: res.state,
19
+ items: (res.orderedProducts ?? []).map(toItem),
20
+ totalPrice: res.totalPrice?.priceTotalPayable,
21
+ totalDiscount: res.totalPrice?.priceDiscount,
22
+ };
23
+ }
24
+ /** Sets quantities (product ID → quantity); 0 removes the product. */
25
+ export function setOrderItems(c, orderId, quantities) {
26
+ const items = [...quantities].map(([productId, quantity]) => ({
27
+ productId,
28
+ quantity,
29
+ originCode: "PRD",
30
+ description: "",
31
+ strikethrough: false,
32
+ }));
33
+ return c.request("/mobile-services/order/v1/items?sortBy=DEFAULT", {
34
+ method: "PUT",
35
+ body: { items },
36
+ orderId,
37
+ });
38
+ }
39
+ export async function getOrderDetails(c, orderId) {
40
+ const res = await c.request(`/mobile-services/order/v1/${orderId}/details-grouped-by-taxonomy`);
41
+ return {
42
+ id: res.orderId,
43
+ state: res.orderState,
44
+ items: (res.groupedProductsInTaxonomy ?? []).flatMap((g) => (g.orderedProducts ?? []).map(toItem)),
45
+ };
46
+ }
47
+ /** Upcoming orders, or past ones (delivered or cancelled) if past; newest first. */
48
+ export async function getFulfillments(c, past) {
49
+ const query = `query Fulfillments {
50
+ orderFulfillments(status: ${past ? "CLOSED" : "OPEN"}) {
51
+ result {
52
+ orderId statusDescription shoppingType modifiable
53
+ totalPrice { totalPrice { amount } }
54
+ delivery { slot { date dateDisplay timeDisplay } }
55
+ }
56
+ }
57
+ }`;
58
+ const data = await c.graphql(query);
59
+ return (data.orderFulfillments.result ?? []).map((f) => ({
60
+ orderId: f.orderId,
61
+ status: f.statusDescription,
62
+ shoppingType: f.shoppingType,
63
+ modifiable: f.modifiable ?? false,
64
+ totalPrice: f.totalPrice?.totalPrice?.amount,
65
+ date: f.delivery?.slot?.date,
66
+ dateDisplay: f.delivery?.slot?.dateDisplay,
67
+ timeDisplay: f.delivery?.slot?.timeDisplay,
68
+ }));
69
+ }
70
+ /** Delivery windows AH offers for address, per day, soonest first. Booking one is only possible in the AH app. */
71
+ export async function getDeliverySlots(c, address) {
72
+ const query = `query DeliverySlots($address: MemberAddressInput!) {
73
+ orderDeliverySlots(address: $address) { date slots { startTime endTime } }
74
+ }`;
75
+ const data = await c.graphql(query, { address: { ...address, houseNumberExtra: address.houseNumberExtra || undefined } });
76
+ return (data.orderDeliverySlots ?? [])
77
+ .map((day) => ({
78
+ date: day.date,
79
+ slots: day.slots
80
+ .map((s) => ({ start: s.startTime, end: s.endTime }))
81
+ .sort((a, b) => a.start.localeCompare(b.start) || a.end.localeCompare(b.end)),
82
+ }))
83
+ .sort((a, b) => a.date.localeCompare(b.date));
84
+ }
85
+ //# sourceMappingURL=orders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orders.js","sourceRoot":"","sources":["../../src/ahapi/orders.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Regular price, and the bonus price for products on bonus (else 0). Deals like
3
+ * "2 voor 1.19" have no single bonus price: AH sends the regular price as both.
4
+ */
5
+ export function productPrices(p) {
6
+ const now = p.currentPrice || p.priceBeforeBonus || 0;
7
+ const regular = p.priceBeforeBonus || now;
8
+ return { regular, bonus: p.isBonus && now < regular ? now : 0 };
9
+ }
10
+ /** Product page on ah.nl: /producten/product/wi<id>/<title slug>. */
11
+ export function productUrl(id, title = "") {
12
+ const slug = title
13
+ .normalize("NFKD")
14
+ .replace(/[\u0300-\u036f]/g, "")
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, "-")
17
+ .replace(/^-|-$/g, "");
18
+ return `https://www.ah.nl/producten/product/wi${id}${slug ? `/${slug}` : ""}`;
19
+ }
20
+ /** URL of the first image, or "". */
21
+ export function productImage(p) {
22
+ return p.images?.[0]?.url ?? "";
23
+ }
24
+ /** Product fields shared by GraphQL queries; map with fromGraphqlProduct. */
25
+ export const GRAPHQL_PRODUCT_FIELDS = "id title brand salesUnitSize priceV2 { now { amount } was { amount } discount { description } }";
26
+ export function fromGraphqlProduct(p) {
27
+ const now = p.priceV2?.now?.amount;
28
+ const was = p.priceV2?.was?.amount;
29
+ const deal = p.priceV2?.discount;
30
+ return {
31
+ webshopId: p.id,
32
+ title: p.title,
33
+ brand: p.brand,
34
+ salesUnitSize: p.salesUnitSize,
35
+ currentPrice: now,
36
+ priceBeforeBonus: was,
37
+ // Multi-buy deals ("2 voor 1.19") keep the regular price, so the deal itself marks a bonus.
38
+ isBonus: Boolean(deal) || (now !== undefined && was !== undefined && now < was),
39
+ bonusMechanism: deal?.description,
40
+ };
41
+ }
42
+ /** Products AH suggests instead of id (similar or substitute products). */
43
+ export async function getProductAlternatives(c, id, size) {
44
+ const query = `query ProductAlternatives($id: Int!, $size: PageSize!) {
45
+ productAlternatives(id: $id, size: $size) { products { ${GRAPHQL_PRODUCT_FIELDS} } }
46
+ }`;
47
+ const data = await c.graphql(query, { id, size });
48
+ return (data.productAlternatives?.products ?? []).map(fromGraphqlProduct);
49
+ }
50
+ /** Largest page AH serves. */
51
+ const MAX_PAGE = 100;
52
+ /** Searches by keyword; returns at most size products. */
53
+ export async function searchProducts(c, query, size, opts = {}) {
54
+ const properties = opts.properties ?? [];
55
+ // AH takes one filter per request (repeated ones get mixed up), so bonus plus properties is filtered here.
56
+ const filterBonus = Boolean(opts.bonus) && properties.length > 0;
57
+ // Sorting low to high lists unavailable products without a price first (30 of 100 for "kaas"); they are dropped.
58
+ const byPrice = opts.sort === "PRICELOWHIGH" || opts.sort === "PRICEHIGHLOW";
59
+ let fetch = size;
60
+ if (opts.sort === "PRICELOWHIGH")
61
+ fetch = MAX_PAGE;
62
+ else if (filterBonus)
63
+ fetch = Math.min(Math.max(size * 4, 40), MAX_PAGE);
64
+ const params = new URLSearchParams({ query, page: "0", size: String(fetch), sortOn: opts.sort ?? "RELEVANCE" });
65
+ // Several properties go comma-separated, which AH combines with AND.
66
+ if (properties.length)
67
+ params.set("filters", `property=${properties.join(",")}`);
68
+ else if (opts.bonus)
69
+ params.set("filters", "bonus=Bonus");
70
+ const res = await c.request(`/mobile-services/product/search/v2?${params}`);
71
+ let products = res.products ?? [];
72
+ if (opts.bonus)
73
+ products = products.filter((p) => p.isBonus);
74
+ if (byPrice) {
75
+ // AH sorts on the regular price; re-sort on what the customer pays now, bonus included.
76
+ const price = (p) => p.currentPrice || p.priceBeforeBonus || 0;
77
+ const dir = opts.sort === "PRICELOWHIGH" ? 1 : -1;
78
+ products = products.filter(price).sort((a, b) => dir * (price(a) - price(b)));
79
+ }
80
+ return products.slice(0, size);
81
+ }
82
+ export async function getProduct(c, id) {
83
+ const res = await c.request(`/mobile-services/product/detail/v4/fir/${id}`);
84
+ return res.productCard;
85
+ }
86
+ /** Returns products in the order of ids. */
87
+ export async function productsByIds(c, ids) {
88
+ const params = new URLSearchParams({ sortOn: "INPUT_PRODUCT_IDS" });
89
+ for (const id of ids)
90
+ params.append("ids", String(id));
91
+ return (await c.request(`/mobile-services/product/search/v2/products?${params}`)) ?? [];
92
+ }
93
+ /** First nutrition table (usually per 100 g/ml), or undefined if AH has none. */
94
+ export async function getNutrition(c, id) {
95
+ const query = `query ProductNutrition($id: Int!) {
96
+ product(id: $id) {
97
+ tradeItem { nutritions { nutrients { type name value } } }
98
+ }
99
+ }`;
100
+ const data = await c.graphql(query, { id });
101
+ return data.product.tradeItem?.nutritions?.[0]?.nutrients;
102
+ }
103
+ //# sourceMappingURL=products.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"products.js","sourceRoot":"","sources":["../../src/ahapi/products.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /** Most recent receipts, newest first. */
2
+ export async function getReceipts(c, limit) {
3
+ const query = `query Receipts($limit: Int!) {
4
+ posReceiptsPage(pagination: {offset: 0, limit: $limit}) {
5
+ posReceipts { id dateTime totalAmount { amount } }
6
+ }
7
+ }`;
8
+ const data = await c.graphql(query, { limit });
9
+ return (data.posReceiptsPage.posReceipts ?? []).map((r) => ({
10
+ id: r.id,
11
+ dateTime: r.dateTime,
12
+ total: r.totalAmount?.amount ?? 0,
13
+ }));
14
+ }
15
+ export async function getReceipt(c, id) {
16
+ const query = `query Receipt($id: String!) {
17
+ posReceiptDetails(id: $id) {
18
+ id
19
+ products { name quantity price { amount } amount { amount } }
20
+ discounts { name amount { amount } }
21
+ payments { method amount { amount } }
22
+ }
23
+ }`;
24
+ const { posReceiptDetails: d } = await c.graphql(query, { id });
25
+ return {
26
+ id: d.id,
27
+ items: (d.products ?? []).map((p) => ({
28
+ name: p.name,
29
+ quantity: p.quantity,
30
+ unitPrice: p.price?.amount,
31
+ total: p.amount?.amount ?? 0,
32
+ })),
33
+ discounts: (d.discounts ?? []).map((x) => ({ name: x.name, amount: x.amount?.amount ?? 0 })),
34
+ payments: (d.payments ?? []).map((x) => ({ method: x.method, amount: x.amount?.amount ?? 0 })),
35
+ };
36
+ }
37
+ //# sourceMappingURL=receipts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipts.js","sourceRoot":"","sources":["../../src/ahapi/receipts.ts"],"names":[],"mappings":"AASA,0CAA0C;AAC1C,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,CAAW,EAAE,KAAa;IAC1D,MAAM,KAAK,GAAG;;;;EAId,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,OAAO,CAEzB,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACrB,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1D,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE,MAAM,IAAI,CAAC;KAClC,CAAC,CAAC,CAAC;AACN,CAAC;AASD,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,CAAW,EAAE,EAAU;IACtD,MAAM,KAAK,GAAG;;;;;;;EAOd,CAAC;IAED,MAAM,EAAE,iBAAiB,EAAE,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,OAAO,CAO7C,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAClB,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACpC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM;YAC1B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;SAC7B,CAAC,CAAC;QACH,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5F,QAAQ,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;KAC/F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,57 @@
1
+ export function recipeUrl(id, slug) {
2
+ return `https://www.ah.nl/allerhande/recept/R-R${id}/${slug}`;
3
+ }
4
+ /** AH returns HTML entities in recipe text, e.g. "1&#189; teen". */
5
+ function decodeEntities(s) {
6
+ const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
7
+ return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, e) => {
8
+ if (e[0] === "#") {
9
+ const code = e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
10
+ return Number.isFinite(code) ? String.fromCodePoint(code) : m;
11
+ }
12
+ return named[e.toLowerCase()] ?? m;
13
+ });
14
+ }
15
+ export async function searchRecipes(c, text, size) {
16
+ const query = `query RecipeSearch($query: RecipeSearchParams!) {
17
+ recipeSearch(query: $query) {
18
+ page { total }
19
+ result { id title slug nutriScore courses time { cook oven wait } serving { number type } rating { average count } }
20
+ }
21
+ }`;
22
+ const data = await c.graphql(query, {
23
+ query: { searchText: text, size },
24
+ });
25
+ return {
26
+ total: data.recipeSearch.page?.total ?? 0,
27
+ recipes: data.recipeSearch.result.map((r) => ({ ...r, title: decodeEntities(r.title) })),
28
+ };
29
+ }
30
+ /** servings scales the ingredient quantities; AH still reports the default in recipe.servings. */
31
+ export async function getRecipe(c, id, servings) {
32
+ const query = `query Recipe($id: Int!, $servings: Int) {
33
+ recipe(id: $id, servings: $servings) {
34
+ id title description href cookTime nutriScore courses cuisines
35
+ servings { number type }
36
+ rating { average count }
37
+ ingredients { text name { singular } }
38
+ preparation { steps }
39
+ nutritions {
40
+ energy { value unit name } carbohydrates { value unit name }
41
+ fat { value unit name } protein { value unit name }
42
+ }
43
+ }
44
+ }`;
45
+ const { recipe: r } = await c.graphql(query, { id, servings });
46
+ return {
47
+ ...r,
48
+ title: decodeEntities(r.title),
49
+ description: r.description && decodeEntities(r.description),
50
+ ingredients: r.ingredients.map((i) => ({
51
+ text: decodeEntities(i.text),
52
+ name: decodeEntities(i.name?.singular || i.text),
53
+ })),
54
+ preparation: r.preparation && { steps: r.preparation.steps?.map(decodeEntities) },
55
+ };
56
+ }
57
+ //# sourceMappingURL=recipes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/ahapi/recipes.ts"],"names":[],"mappings":"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"}