import PicnicClient from "picnic-api" import type { ApiConfig, Decorator, Delivery } from "picnic-api/lib/types/picnic-api.js" import type { ParsedArgs } from 'minimist' import { DateTime } from "luxon" import fs from "node:fs" import { jsonOut, csvOut, sum, runCommands, getXdgConfigHome } from "./util.js" import path from "node:path" import repl from "node:repl" namespace PicnicExt { export type PromoDecorator = { type: "PROMO"; text: string }; export type ExtendedDecorator = Decorator | PromoDecorator export interface Decorated { decorators?: Decorator[] } } function getDecorator(thing: PicnicExt.Decorated, name: "PROMO"): string | undefined function getDecorator(thing: PicnicExt.Decorated, name: "QUANTITY"): number | undefined function getDecorator(thing: PicnicExt.Decorated, name: "PRICE"): number | undefined function getDecorator(thing: PicnicExt.Decorated, name: "PROMO", fallback: string): string function getDecorator(thing: PicnicExt.Decorated, name: "QUANTITY", fallback: number): number function getDecorator(thing: PicnicExt.Decorated, name: "PRICE", fallback: number): number function getDecorator( thing: PicnicExt.Decorated, name: string, fallback?: number | string ): number | string | undefined { if (!thing.decorators) return fallback const j = thing.decorators as PicnicExt.ExtendedDecorator[] const l = j.filter(x => x.type == name) if (l.length > 0 && l[0].type == name) { switch (l[0].type) { case "QUANTITY": return l[0].quantity case "PRICE": return l[0].display_price case "PROMO": return l[0].text } } return fallback } function deliveryToCSV(delv: Delivery) { const delivered = DateTime.fromISO(delv.slot.window_start).toFormat( "yyyy-MM-dd HH:mm" ) for (const order of delv.orders) { const ordered = DateTime.fromISO(order.creation_time).toFormat( "yyyy-MM-dd HH:mm" ) for (const orderline of order.items) { const real_price = getDecorator(orderline, "PRICE", orderline.display_price) const totalprice = sum(orderline.items.map(x => x.price)) const promo = real_price / totalprice for (const orderarticle of orderline.items) { csvOut({ id: orderarticle.id, name: orderarticle.name, quantity: getDecorator(orderarticle, "QUANTITY", 1), price: Math.round(orderarticle.price * promo), promo: (1 - real_price / orderline.display_price).toFixed(2), unit_quantity: orderarticle.unit_quantity, ordered, delivered, delivery_id: delv.delivery_id, }) } } } } const commands = (argv: ParsedArgs) => { const LOGIN_FILE = path.join(getXdgConfigHome(), "picnic-cli.json") const config: ApiConfig = fs.existsSync(LOGIN_FILE) ? JSON.parse(fs.readFileSync(LOGIN_FILE, "utf8")) : {} const p = new PicnicClient(config) return { async login(user: string, password: string) { await p.login(user, password) fs.writeFileSync( LOGIN_FILE, JSON.stringify({ countryCode: p.countryCode, authKey: p.authKey, } as ApiConfig) ) }, async getUserDetails() { jsonOut(await p.getUserDetails()) }, async deliveries() { const deliveries = await p.getDeliveries() if (argv.json) { jsonOut(deliveries) } else { for (const delv of deliveries) { if (delv.status == "CANCELLED") continue const dt = DateTime.fromISO( delv.delivery_time ? delv.delivery_time.start : delv.slot.window_start ) csvOut({ id: delv.delivery_id, delivered: dt.toFormat("yyyy-MM-dd HH:mm"), status: delv.status, price: sum(delv.orders.map(x => x.total_price)), }) } } }, async delivery(...ids: string[]) { for (const deliveryId of ids) { const delv = await p.getDelivery(deliveryId) if (argv.json) { jsonOut(delv) } else { deliveryToCSV(delv) } } }, async article(id: string) { jsonOut(await p.getArticle(id)) }, async "cart.list"() { const cart = await p.getShoppingCart() if (argv.json) { jsonOut(cart) } else { const out: any[] = [] cart.items.map(orderLine => { const isGroup = orderLine.items.length > 1 if (isGroup) { out.push({ name: ``, price: getDecorator(orderLine, "PRICE", orderLine.price) }) } for (const item of orderLine.items) { out.push({ id: item.id, name: item.name, quantity: getDecorator(item, "QUANTITY", 1), price: item.price }) } if (isGroup) out.push({name: ""}) }) console.table(out); } }, async "cart.add"(product: string, count: string = "1") { if (isNaN(+count)) throw new TypeError("count must be a number") await p.addProductToShoppingCart(product, +count) }, async "cart.del"(product: string, count: string = "1") { if (isNaN(+count)) throw new TypeError("count must be a number") await p.removeProductFromShoppingCart(product, +count) }, repl() { repl.start().context.p = p } } } runCommands(commands)