178 lines
5.4 KiB
TypeScript
178 lines
5.4 KiB
TypeScript
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: `<Group ${getDecorator(orderLine, "PROMO", "???")}>`,
|
|
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: "</Group>"})
|
|
})
|
|
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)
|