ebag 0.1.0 → 0.1.2

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 CHANGED
@@ -23,9 +23,13 @@ ebag login --cookie "<cookie>"
23
23
  ```
24
24
 
25
25
  Notes:
26
+
26
27
  - Session data is stored in plain text at `~/.config/ebag/session.json` with owner-only permissions (`0600`).
27
28
  - List-based search caches product details in `~/.config/ebag/cache.json` for up to 6 hours.
28
29
  - Completed orders may be cached locally for faster access.
30
+ - Unknown order statuses warn on stderr and include a link to file a GitHub issue.
31
+ - API `statusText` values are inconsistent with the website and intentionally ignored (including `--json`).
32
+ - `statusDescription` is the supported status label field in `--json` output and is computed at runtime.
29
33
 
30
34
  ## Products
31
35
 
@@ -73,18 +77,37 @@ ebag list show 753250
73
77
  npm run build
74
78
  ```
75
79
 
76
- ### End-to-end tests
80
+ ### Unit tests
81
+
82
+ ```bash
83
+ npm run test:unit
84
+ ```
77
85
 
78
- By default, `npm run test:e2e` reads cookies from `tests/.secrets/ebag-cookies` (gitignored).
79
- You can also override with `EBAG_COOKIE`.
86
+ ### End-to-end tests
80
87
 
81
- Optional query override:
88
+ Requires a valid session in `tests/.config/ebag/session.json` (gitignored):
82
89
 
83
90
  ```bash
84
- EBAG_COOKIE="<cookie>" EBAG_TEST_QUERY="lindt" npm run test:e2e
91
+ npm run test:e2e
85
92
  ```
86
- Store test cookies in `tests/.secrets/ebag-cookies` (gitignored), then run:
93
+
94
+ ### All tests
87
95
 
88
96
  ```bash
89
- npm run test:e2e
97
+ npm test
90
98
  ```
99
+
100
+ ## Changelog
101
+
102
+ ### 0.1.2
103
+
104
+ - Log unknown order statuses in the lib layer and warn in the CLI
105
+ - Add tests for order status logging and CLI warnings
106
+
107
+ ### 0.1.1
108
+
109
+ - Add logging in the config directory
110
+
111
+ ### 0.1.0
112
+
113
+ - Initial release
@@ -1,4 +1,4 @@
1
- import type { OrderDetail, OrderSummary, ProductSummary } from '../lib/types';
1
+ import type { OrderDetail, OrderSummary, ProductSummary } from "../lib/types";
2
2
  export declare function outputJson(data: unknown): void;
3
3
  export declare function formatHeading(text: string): string;
4
4
  export declare function outputProducts(products: ProductSummary[]): void;
@@ -7,6 +7,7 @@ exports.outputList = outputList;
7
7
  exports.outputProductDetail = outputProductDetail;
8
8
  exports.outputOrdersList = outputOrdersList;
9
9
  exports.outputOrderDetail = outputOrderDetail;
10
+ const order_status_1 = require("../lib/order-status");
10
11
  function outputJson(data) {
11
12
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
12
13
  }
@@ -23,84 +24,95 @@ function formatPrice(product) {
23
24
  return product.pricePromo;
24
25
  if (product.price)
25
26
  return product.price;
26
- return '';
27
+ return "";
27
28
  }
28
29
  function outputProducts(products) {
29
30
  for (const product of products) {
30
31
  const price = formatPrice(product);
31
- let source = '';
32
- if (product.source === 'list') {
32
+ let source = "";
33
+ if (product.source === "list") {
33
34
  const listNames = (product.listNames || []).filter(Boolean);
34
35
  if (listNames.length) {
35
- source = ` [${listNames.join(', ')}]`;
36
+ source = ` [${listNames.join(", ")}]`;
36
37
  }
37
38
  }
38
- const line = `${product.id} ${product.name}${price ? ` - ${price}` : ''}${source}`;
39
+ const line = `${product.id} ${product.name}${price ? ` - ${price}` : ""}${source}`;
39
40
  process.stdout.write(`${line}\n`);
40
41
  }
41
42
  }
42
43
  function outputList(items) {
43
44
  for (const item of items) {
44
- const count = item.count !== undefined ? ` (${item.count})` : '';
45
+ const count = item.count !== undefined ? ` (${item.count})` : "";
45
46
  process.stdout.write(`${item.id} ${item.name}${count}\n`);
46
47
  }
47
48
  }
48
49
  function outputProductDetail(data) {
49
- const id = data.id ? String(data.id) : '';
50
- const name = data.name ? String(data.name) : '';
51
- const nameEn = data.name_en ? String(data.name_en) : '';
52
- const brand = typeof data.brand === 'string'
50
+ const id = data.id ? String(data.id) : "";
51
+ const name = data.name ? String(data.name) : "";
52
+ const nameEn = data.name_en ? String(data.name_en) : "";
53
+ const brand = typeof data.brand === "string"
53
54
  ? data.brand
54
55
  : data.brand?.name ||
55
- data.brand
56
- ?.name_bg ||
57
- data.brand
58
- ?.name_en ||
59
- '';
60
- const unit = data.unit_weight_text ? String(data.unit_weight_text) : '';
56
+ data.brand?.name_bg ||
57
+ data.brand?.name_en ||
58
+ "";
59
+ const unit = data.unit_weight_text ? String(data.unit_weight_text) : "";
61
60
  const price = data.current_price_eur ?? data.price_promo_eur ?? data.price_eur;
62
- const priceText = price ? String(price) : '';
63
- const currency = priceText ? 'EUR' : '';
64
- const availability = data.is_available === false ? 'No' : data.is_available === true ? 'Yes' : '';
65
- const urlSlug = data.url_slug ? String(data.url_slug) : '';
66
- const origin = data.country_of_origin ? String(data.country_of_origin) : '';
67
- const expiryRaw = data.expiry_date ? String(data.expiry_date) : '';
61
+ const priceText = price ? String(price) : "";
62
+ const currency = priceText ? "EUR" : "";
63
+ const availability = data.is_available === false
64
+ ? "No"
65
+ : data.is_available === true
66
+ ? "Yes"
67
+ : "";
68
+ const urlSlug = data.url_slug ? String(data.url_slug) : "";
69
+ const origin = data.country_of_origin ? String(data.country_of_origin) : "";
70
+ const expiryRaw = data.expiry_date ? String(data.expiry_date) : "";
68
71
  const expiry = formatDate(expiryRaw);
69
- const description = data.description ? String(data.description) : '';
72
+ const description = data.description ? String(data.description) : "";
70
73
  const kvPairs = [
71
- ['ID', id],
72
- ['Brand', brand],
73
- ['Unit', unit],
74
- ['Price', priceText ? `${priceText}${currency ? ` ${currency}` : ''}` : ''],
75
- ['Available', availability],
76
- ['Origin', origin],
77
- ['Expiry', expiry],
78
- ['Slug', urlSlug],
74
+ ["ID", id],
75
+ ["Brand", brand],
76
+ ["Unit", unit],
77
+ ["Price", priceText ? `${priceText}${currency ? ` ${currency}` : ""}` : ""],
78
+ ["Available", availability],
79
+ ["Origin", origin],
80
+ ["Expiry", expiry],
81
+ ["Slug", urlSlug],
79
82
  ];
80
83
  const energyPairs = formatEnergyValues(data.energy_values);
81
- const kvLines = [
82
- ...kvPairs.filter(([, value]) => value),
83
- ...energyPairs,
84
- ].map(([key, value]) => `${key}: ${value}`);
85
- const headerLines = [`# ${name || 'Product'}`, nameEn ? `*${nameEn}*` : ''].filter(Boolean);
86
- const descriptionMd = description ? htmlToMarkdown(description) : '';
84
+ const kvLines = [...kvPairs.filter(([, value]) => value), ...energyPairs].map(([key, value]) => `${key}: ${value}`);
85
+ const headerLines = [
86
+ `# ${name || "Product"}`,
87
+ nameEn ? `*${nameEn}*` : "",
88
+ ].filter(Boolean);
89
+ const descriptionMd = description ? htmlToMarkdown(description) : "";
87
90
  const { descriptionText, ingredientsText } = splitIngredients(descriptionMd);
88
- const descriptionBlock = descriptionText ? `# Description\n\n${descriptionText}` : '';
89
- const ingredientsBlock = ingredientsText ? `\n# Ingredients\n\n${ingredientsText}` : '';
90
- const kvBlock = kvLines.length ? ['---', ...kvLines, '---'].join('\n') : '';
91
- const output = [headerLines.join('\n'), kvBlock, descriptionBlock, ingredientsBlock]
91
+ const descriptionBlock = descriptionText
92
+ ? `# Description\n\n${descriptionText}`
93
+ : "";
94
+ const ingredientsBlock = ingredientsText
95
+ ? `\n# Ingredients\n\n${ingredientsText}`
96
+ : "";
97
+ const kvBlock = kvLines.length ? ["---", ...kvLines, "---"].join("\n") : "";
98
+ const output = [
99
+ headerLines.join("\n"),
100
+ kvBlock,
101
+ descriptionBlock,
102
+ ingredientsBlock,
103
+ ]
92
104
  .filter(Boolean)
93
- .join('\n');
105
+ .join("\n");
94
106
  process.stdout.write(`${output}\n`);
95
107
  }
96
108
  function formatOrderAmount(order) {
97
- if ('finalAmountEur' in order && order.finalAmountEur) {
109
+ if ("finalAmountEur" in order && order.finalAmountEur) {
98
110
  return `${order.finalAmountEur} EUR`;
99
111
  }
100
- if ('finalAmount' in order && order.finalAmount) {
112
+ if ("finalAmount" in order && order.finalAmount) {
101
113
  return order.finalAmount;
102
114
  }
103
- if ('totals' in order) {
115
+ if ("totals" in order) {
104
116
  if (order.totals.totalPaidEur)
105
117
  return `${order.totals.totalPaidEur} EUR`;
106
118
  if (order.totals.totalEur)
@@ -110,50 +122,52 @@ function formatOrderAmount(order) {
110
122
  if (order.totals.total)
111
123
  return order.totals.total;
112
124
  }
113
- return '';
125
+ return "";
114
126
  }
115
127
  function formatDateInTimeZone(date, timeZone) {
116
- const parts = new Intl.DateTimeFormat('en-US', {
128
+ const parts = new Intl.DateTimeFormat("en-US", {
117
129
  timeZone,
118
- year: 'numeric',
119
- month: '2-digit',
120
- day: '2-digit',
130
+ year: "numeric",
131
+ month: "2-digit",
132
+ day: "2-digit",
121
133
  }).formatToParts(date);
122
134
  const lookup = new Map(parts.map((part) => [part.type, part.value]));
123
- const year = lookup.get('year') || '';
124
- const month = lookup.get('month') || '';
125
- const day = lookup.get('day') || '';
135
+ const year = lookup.get("year") || "";
136
+ const month = lookup.get("month") || "";
137
+ const day = lookup.get("day") || "";
126
138
  return `${year}-${month}-${day}`;
127
139
  }
128
140
  function formatOrderStatus(order) {
129
- const status = order.status;
130
- if (status === 3)
131
- return 'Отказана';
132
- if (status === 4) {
133
- const date = order.shippingDate ? formatDate(order.shippingDate) : '';
134
- const todayDate = formatDateInTimeZone(new Date(), 'Europe/Sofia');
135
- if (date && date >= todayDate)
136
- return 'Нова';
137
- return 'Завършена';
138
- }
139
- if (status !== undefined) {
140
- return `Status ${status}`;
141
- }
142
- return '';
141
+ if (order.statusDescription)
142
+ return order.statusDescription;
143
+ return (0, order_status_1.describeOrderStatus)(order.status);
143
144
  }
144
145
  function outputOrdersList(orders) {
145
146
  for (const order of orders) {
146
- const date = order.shippingDate ? formatDate(order.shippingDate) : '';
147
- const slot = order.timeSlotDisplay || '';
147
+ const date = order.shippingDate ? formatDate(order.shippingDate) : "";
148
+ const slot = order.timeSlotDisplay || "";
148
149
  const status = formatOrderStatus(order);
149
150
  const total = formatOrderAmount(order);
150
151
  const parts = [order.id, date, slot].filter(Boolean);
151
- const suffix = [status, total].filter(Boolean).join(' - ');
152
- const line = `${parts.join(' ')}${suffix ? ` - ${suffix}` : ''}`;
152
+ const suffix = [status, total].filter(Boolean).join(" - ");
153
+ const line = `${parts.join(" ")}${suffix ? ` - ${suffix}` : ""}`;
153
154
  process.stdout.write(`${line}\n`);
154
155
  }
155
156
  }
156
157
  function outputOrderItems(items) {
158
+ const formatQuantity = (quantity) => {
159
+ if (!quantity)
160
+ return "";
161
+ const normalized = quantity.trim().replace(",", ".");
162
+ if (!normalized)
163
+ return "";
164
+ const parsed = Number(normalized);
165
+ if (!Number.isFinite(parsed))
166
+ return normalized;
167
+ if (Number.isInteger(parsed))
168
+ return String(parsed);
169
+ return String(parsed);
170
+ };
157
171
  const byGroup = new Map();
158
172
  const ungrouped = [];
159
173
  for (const item of items) {
@@ -171,57 +185,69 @@ function outputOrderItems(items) {
171
185
  for (const [group, groupItems] of groupEntries) {
172
186
  process.stdout.write(`## ${group}\n`);
173
187
  for (const item of groupItems) {
174
- const qty = item.quantity ? ` x${item.quantity}` : '';
175
- const unit = item.unit ? ` (${item.unit})` : '';
176
- const price = item.priceEur ? `${item.priceEur} EUR` : item.price || '';
177
- const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ''}`;
188
+ const qtyValue = formatQuantity(item.quantity);
189
+ const qty = qtyValue ? ` x${qtyValue}` : "";
190
+ const unit = item.unit ? ` (${item.unit})` : "";
191
+ const price = item.priceEur ? `${item.priceEur} EUR` : item.price || "";
192
+ const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ""}`;
178
193
  process.stdout.write(`${line}\n`);
179
194
  }
180
- process.stdout.write('\n');
195
+ process.stdout.write("\n");
181
196
  }
182
197
  }
183
198
  if (ungrouped.length) {
184
199
  for (const item of ungrouped) {
185
- const qty = item.quantity ? ` x${item.quantity}` : '';
186
- const unit = item.unit ? ` (${item.unit})` : '';
187
- const price = item.priceEur ? `${item.priceEur} EUR` : item.price || '';
188
- const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ''}`;
200
+ const qtyValue = formatQuantity(item.quantity);
201
+ const qty = qtyValue ? ` x${qtyValue}` : "";
202
+ const unit = item.unit ? ` (${item.unit})` : "";
203
+ const price = item.priceEur ? `${item.priceEur} EUR` : item.price || "";
204
+ const line = `- ${item.name}${unit}${qty}${price ? ` - ${price}` : ""}`;
189
205
  process.stdout.write(`${line}\n`);
190
206
  }
191
- process.stdout.write('\n');
207
+ process.stdout.write("\n");
192
208
  }
193
209
  }
194
210
  function outputOrderDetail(detail) {
195
- const header = `# Order ${detail.id || ''}`.trim();
211
+ const header = `# Order ${detail.id || ""}`.trim();
196
212
  const status = formatOrderStatus(detail);
197
- const date = detail.shippingDate ? formatDate(detail.shippingDate) : '';
198
- const address = detail.address || '';
213
+ const date = detail.shippingDate ? formatDate(detail.shippingDate) : "";
214
+ const address = detail.address || "";
199
215
  const total = formatOrderAmount(detail);
200
216
  const kvPairs = [
201
- ['ID', detail.id],
202
- ['Status', status],
203
- ['Date', date],
204
- ['Timeslot', detail.timeSlotDisplay || ''],
205
- ['Address', address],
206
- ['Total', total],
207
- ['Discount', detail.totals.discountEur ? `${detail.totals.discountEur} EUR` : detail.totals.discount || ''],
208
- ['Tip', detail.totals.tipEur ? `${detail.totals.tipEur} EUR` : detail.totals.tip || ''],
217
+ ["ID", detail.id],
218
+ ["Status", status],
219
+ ["Date", date],
220
+ ["Timeslot", detail.timeSlotDisplay || ""],
221
+ ["Address", address],
222
+ ["Total", total],
223
+ [
224
+ "Discount",
225
+ detail.totals.discountEur
226
+ ? `${detail.totals.discountEur} EUR`
227
+ : detail.totals.discount || "",
228
+ ],
229
+ [
230
+ "Tip",
231
+ detail.totals.tipEur
232
+ ? `${detail.totals.tipEur} EUR`
233
+ : detail.totals.tip || "",
234
+ ],
209
235
  ].filter(([, value]) => value);
210
236
  const kvLines = kvPairs.map(([key, value]) => `${key}: ${value}`);
211
- const kvBlock = kvLines.length ? ['---', ...kvLines, '---'].join('\n') : '';
237
+ const kvBlock = kvLines.length ? ["---", ...kvLines, "---"].join("\n") : "";
212
238
  process.stdout.write(`${header}\n`);
213
239
  if (kvBlock) {
214
240
  process.stdout.write(`${kvBlock}\n`);
215
241
  }
216
- process.stdout.write('# Items\n');
242
+ process.stdout.write("# Items\n");
217
243
  if (detail.items.length) {
218
244
  outputOrderItems(detail.items);
219
245
  }
220
246
  else {
221
- process.stdout.write('No items found.\n\n');
247
+ process.stdout.write("No items found.\n\n");
222
248
  }
223
249
  if (detail.additionalOrders && detail.additionalOrders.length) {
224
- process.stdout.write('# Additional Orders\n');
250
+ process.stdout.write("# Additional Orders\n");
225
251
  for (const additional of detail.additionalOrders) {
226
252
  process.stdout.write(`## Order ${additional.id}\n`);
227
253
  const additionalTotal = formatOrderAmount(additional);
@@ -232,14 +258,14 @@ function outputOrderDetail(detail) {
232
258
  outputOrderItems(additional.items);
233
259
  }
234
260
  else {
235
- process.stdout.write('No items found.\n\n');
261
+ process.stdout.write("No items found.\n\n");
236
262
  }
237
263
  }
238
264
  }
239
265
  }
240
266
  function formatDate(value) {
241
267
  if (!value)
242
- return '';
268
+ return "";
243
269
  const isoMatch = value.match(/(\d{4})-(\d{2})-(\d{2})/);
244
270
  if (isoMatch)
245
271
  return `${isoMatch[1]}-${isoMatch[2]}-${isoMatch[3]}`;
@@ -252,7 +278,7 @@ function formatDate(value) {
252
278
  const parsed = new Date(value);
253
279
  if (Number.isNaN(parsed.getTime()))
254
280
  return value;
255
- const pad = (n) => String(n).padStart(2, '0');
281
+ const pad = (n) => String(n).padStart(2, "0");
256
282
  return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}`;
257
283
  }
258
284
  function formatEnergyValues(value) {
@@ -261,7 +287,7 @@ function formatEnergyValues(value) {
261
287
  if (Array.isArray(value)) {
262
288
  const pairs = [];
263
289
  for (const item of value) {
264
- if (!item || typeof item !== 'object')
290
+ if (!item || typeof item !== "object")
265
291
  continue;
266
292
  const entry = item;
267
293
  const label = entry.name_bg ||
@@ -279,7 +305,7 @@ function formatEnergyValues(value) {
279
305
  }
280
306
  return pairs;
281
307
  }
282
- if (typeof value === 'object') {
308
+ if (typeof value === "object") {
283
309
  return Object.entries(value)
284
310
  .filter(([, entry]) => entry !== null && entry !== undefined)
285
311
  .flatMap(([key, entry]) => splitEnergyValues(`Energy ${key}`, String(entry)));
@@ -292,7 +318,7 @@ function splitEnergyValues(label, raw) {
292
318
  const kjMatch = normalized.match(/(\d+(?:\.\d+)?)\s*kJ/i);
293
319
  const hasKcalKjLabel = /kcal/i.test(label) && /kJ/i.test(label);
294
320
  const splitMatch = normalized.match(/^(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)$/);
295
- const baseLabel = label.replace(/\(?\s*kcal\s*\/\s*kJ\s*\)?/i, '').trim();
321
+ const baseLabel = label.replace(/\(?\s*kcal\s*\/\s*kJ\s*\)?/i, "").trim();
296
322
  const energyLabel = baseLabel || label;
297
323
  if (kcalMatch || kjMatch) {
298
324
  const items = [];
@@ -311,40 +337,40 @@ function splitEnergyValues(label, raw) {
311
337
  return [[label, normalized]];
312
338
  }
313
339
  function normalizeNumberString(value) {
314
- if (typeof value === 'number')
340
+ if (typeof value === "number")
315
341
  return value.toString();
316
- if (typeof value !== 'string')
342
+ if (typeof value !== "string")
317
343
  return String(value);
318
- const trimmed = value.trim().replace(/\u00a0/g, ' ');
319
- const noSpaces = trimmed.replace(/\s+/g, '');
320
- if (noSpaces.includes(',')) {
321
- return noSpaces.replace(',', '.');
344
+ const trimmed = value.trim().replace(/\u00a0/g, " ");
345
+ const noSpaces = trimmed.replace(/\s+/g, "");
346
+ if (noSpaces.includes(",")) {
347
+ return noSpaces.replace(",", ".");
322
348
  }
323
349
  return noSpaces;
324
350
  }
325
351
  function htmlToMarkdown(html) {
326
352
  let text = html;
327
- text = text.replace(/<\s*br\s*\/?\s*>/gi, '\n');
328
- text = text.replace(/<\s*\/p\s*>/gi, '\n\n');
329
- text = text.replace(/<\s*p[^>]*>/gi, '');
330
- text = text.replace(/<\s*li[^>]*>/gi, '- ');
331
- text = text.replace(/<\s*\/li\s*>/gi, '\n');
332
- text = text.replace(/<\s*ul[^>]*>/gi, '\n');
333
- text = text.replace(/<\s*\/ul\s*>/gi, '\n');
334
- text = text.replace(/<[^>]+>/g, '');
335
- text = text.replace(/&nbsp;/g, ' ');
336
- text = text.replace(/&amp;/g, '&');
337
- text = text.replace(/&lt;/g, '<');
338
- text = text.replace(/&gt;/g, '>');
339
- text = text.replace(/\n{3,}/g, '\n\n');
353
+ text = text.replace(/<\s*br\s*\/?\s*>/gi, "\n");
354
+ text = text.replace(/<\s*\/p\s*>/gi, "\n\n");
355
+ text = text.replace(/<\s*p[^>]*>/gi, "");
356
+ text = text.replace(/<\s*li[^>]*>/gi, "- ");
357
+ text = text.replace(/<\s*\/li\s*>/gi, "\n");
358
+ text = text.replace(/<\s*ul[^>]*>/gi, "\n");
359
+ text = text.replace(/<\s*\/ul\s*>/gi, "\n");
360
+ text = text.replace(/<[^>]+>/g, "");
361
+ text = text.replace(/&nbsp;/g, " ");
362
+ text = text.replace(/&amp;/g, "&");
363
+ text = text.replace(/&lt;/g, "<");
364
+ text = text.replace(/&gt;/g, ">");
365
+ text = text.replace(/\n{3,}/g, "\n\n");
340
366
  return text.trim();
341
367
  }
342
368
  function splitIngredients(text) {
343
369
  if (!text)
344
- return { descriptionText: '', ingredientsText: '' };
370
+ return { descriptionText: "", ingredientsText: "" };
345
371
  const match = text.match(/(?:^|\n)\s*Съставки\s*:?\s*/i);
346
372
  if (!match || match.index === undefined) {
347
- return { descriptionText: text, ingredientsText: '' };
373
+ return { descriptionText: text, ingredientsText: "" };
348
374
  }
349
375
  const start = match.index;
350
376
  const end = start + match[0].length;