Pantry

model.js 234 lines

Files · 8
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234import ddl from "./schema.js";
import manifest from "./manifest.json";
import { database } from "../../shared/database.js";
import { localDate } from "../../shared/data.js";
import { addDays } from "../../shared/format.js";
export function createPantry(L) {
  const db = database(L, ddl, manifest);
  const product = (data) => {
    db.check("add-product", data);
    if (!data.name.trim()) throw Error("Enter a product name.");
    return data;
  };
  const units = async (data) => {
    for (const key of ["qu_id_purchase", "qu_id_stock"])
      if (data[key] != null && !(await db.get("quantity_units", data[key])))
        throw Error("Choose an existing quantity unit.");
  };
  const addProduct = async (data) => {
    product(data);
    await units(data);
    const row = await db.insert("products", data);
    await db.emit("product-added", row);
    return row;
  };
  const prepareStock = async (data) => {
    db.check("add-stock", data);
    const p = await db.get("products", data.product_id);
    if (!p) throw Error("Choose an existing product.");
    const value = { ...data };
    if (value.purchased_date === undefined) value.purchased_date = localDate();
    if (value.location_id === undefined) value.location_id = p.location_id;
    if (value.best_before_date === undefined && p.default_best_before_days > 0)
      value.best_before_date = addDays(
        value.purchased_date || localDate(),
        p.default_best_before_days,
      );
    if (value.open === 1 && !value.opened_date) value.opened_date = localDate();
    if (value.open === 1 && p.default_best_before_days_after_open > 0) {
      const deadline = addDays(
        value.opened_date,
        p.default_best_before_days_after_open,
      );
      if (!value.best_before_date || deadline < value.best_before_date)
        value.best_before_date = deadline;
    }
    return value;
  };
  const addStock = async (data) => {
    const row = await db.insert("stock_entries", await prepareStock(data));
    await db.emit("stock-added", row);
    return row;
  };
  return {
    ...db,
    list: () =>
      db.ready.then(async () => ({
        products: await db.query(
          "SELECT * FROM products ORDER BY name COLLATE NOCASE",
        ),
        lots: await db.query(
          "SELECT * FROM stock_entries ORDER BY best_before_date IS NULL, best_before_date, id",
        ),
        units: await db.query(
          "SELECT * FROM quantity_units ORDER BY name COLLATE NOCASE",
        ),
      })),
    addProduct: (data) => db.run(() => addProduct(data)),
    addStock: (data) => db.run(() => addStock(data)),
    editProduct: (id, data) =>
      db.run(async () => {
        product(data);
        await units(data);
        return db.update("products", id, data);
      }),
    editStock: (id, data) =>
      db.run(async () => {
        const old = await db.get("stock_entries", id);
        if (!old) throw Error("Stock lot no longer exists.");
        if (data.stock_id !== undefined && data.stock_id !== old.stock_id)
          throw Error("A stock lot keeps its stable stock ID.");
        if (data.product_id !== old.product_id)
          throw Error("A stock lot keeps its product.");
        const value = await prepareStock(data);
        const row = await db.update("stock_entries", id, value);
        if (!old.open && row.open) await db.emit("stock-opened", row);
        return row;
      }),
    unit: (data, id) =>
      db.run(async () => {
        if (!data.name?.trim()) throw Error("Enter a unit name.");
        return id === undefined
          ? db.insert("quantity_units", data)
          : db.update("quantity_units", id, data);
      }),
    removeUnit: (id) =>
      db.run(async () => {
        if (
          (
            await db.query(
              "SELECT id FROM products WHERE qu_id_stock = ? OR qu_id_purchase = ?",
              [id, id],
            )
          ).length
        )
          throw Error(
            "This unit is used by a product. Change the product units first.",
          );
        await db.exec("DELETE FROM quantity_units WHERE id = ?", [id]);
      }),
    removeProduct: (id) =>
      db.run(async () => {
        if (
          (
            await db.query(
              "SELECT id FROM stock_entries WHERE product_id = ?",
              [id],
            )
          ).length
        )
          throw Error("Remove this product’s stock lots first.");
        await db.exec("DELETE FROM products WHERE id = ?", [id]);
      }),
    removeStock: (id) =>
      db.run(() => db.exec("DELETE FROM stock_entries WHERE id = ?", [id])),
    consume: (id, amount, note) =>
      db.run(async () => {
        const row = await db.get("stock_entries", id);
        if (!row) throw Error("Stock lot no longer exists.");
        if (!Number.isFinite(amount) || amount <= 0 || amount > row.amount)
          throw Error(
            "Enter a positive amount no greater than the lot balance.",
          );
        const remainder = row.amount - amount;
        if (amount === row.amount)
          await db.exec("DELETE FROM stock_entries WHERE id = ?", [id]);
        else await db.update("stock_entries", id, { amount: remainder });
        await db.emit("stock-consumed", {
          product_id: row.product_id,
          stock_id: row.stock_id,
          amount,
          note: note ?? row.note,
        });
      }),
    open: (id) =>
      db.run(async () => {
        const row = await db.get("stock_entries", id);
        if (!row) throw Error("Stock lot no longer exists.");
        if (row.open) return;
        const p = await db.get("products", row.product_id),
          opened_date = localDate();
        let best_before_date = row.best_before_date;
        if (p.default_best_before_days_after_open > 0) {
          const deadline = addDays(
            opened_date,
            p.default_best_before_days_after_open,
          );
          if (!best_before_date || deadline < best_before_date)
            best_before_date = deadline;
        }
        const updated = await db.update("stock_entries", id, {
          open: 1,
          opened_date,
          best_before_date,
        });
        await db.emit("stock-opened", updated);
      }),
    sample: () =>
      db.run(async () => {
        const kg = await db.insert("quantity_units", {
            name: "kg",
            name_plural: "kg",
            description: "Kilograms",
          }),
          bottle = await db.insert("quantity_units", {
            name: "bottle",
            name_plural: "bottles",
          }),
          bag = await db.insert("quantity_units", {
            name: "bag",
            name_plural: "bags",
          });
        const oats = await addProduct({
            name: "Rolled oats",
            description: "Breakfast cupboard",
            qu_id_purchase: bag.id,
            qu_id_stock: kg.id,
            min_stock_amount: 0.5,
            default_best_before_days: 180,
          }),
          milk = await addProduct({
            name: "Oat milk",
            description: "Unsweetened · fridge after opening",
            qu_id_purchase: bottle.id,
            qu_id_stock: bottle.id,
            min_stock_amount: 2,
            default_best_before_days: 90,
            default_best_before_days_after_open: 5,
          }),
          lentils = await addProduct({
            name: "Red lentils",
            description: "Dry goods",
            qu_id_purchase: bag.id,
            qu_id_stock: kg.id,
            min_stock_amount: 1,
            default_best_before_days: 365,
          });
        await addStock({
          product_id: oats.id,
          amount: 0.75,
          best_before_date: addDays(localDate(), 45),
          note: "Top shelf",
        });
        await addStock({
          product_id: milk.id,
          amount: 1,
          best_before_date: addDays(localDate(), 2),
          open: 1,
          opened_date: localDate(),
          note: "Opened for breakfast",
        });
        await addStock({
          product_id: milk.id,
          amount: 1,
          best_before_date: addDays(localDate(), 70),
          note: "Unopened reserve",
        });
        await addStock({
          product_id: lentils.id,
          amount: 0.35,
          best_before_date: addDays(localDate(), 110),
        });
      }),
  };
}