Test file

Ledger invariants

The stock rules on their own: a balance always equals its movements, a version applies once, an oversell opens one incident, and refunds can’t add stock.

tests/inventory.test.ts · 183 lines

import { describe, it, expect } from "vitest";
import fc from "fast-check";
import {
  seedDomain,
  balance,
  movement,
  applySale,
  applyRefund,
  resolveIncident,
  type DomainState,
  type Sale,
} from "../packages/domain/src";

const now = "2026-10-11T10:00:00Z";
// A new workspace opens with 30 bags of each product at the roastery and
// 20 at the cafe.
const opening = 30;

const sale = (
  key: string,
  quantity: number,
  occurredAt = now,
  updatedAt = now,
): Sale => ({
  key,
  source: key.startsWith("square") ? "square" : "shopify",
  sourceId: key,
  locationId: "roastery",
  updatedAt,
  occurredAt,
  lines: [{ sku: "espresso-250", quantity }],
  cause: key,
  cancelled: false,
  // The till's sales here were made while it was offline.
  offline: key.startsWith("square"),
});

const espresso = (state: DomainState, location = "roastery") =>
  balance(state, "espresso-250", location);

describe("append-only inventory and oversells", () => {
  it("keeps the balance equal to the sum of any sequence of movements", () => {
    fc.assert(
      fc.property(
        fc.array(fc.integer({ min: -20, max: 20 }), { maxLength: 60 }),
        (deltas) => {
          const state = seedDomain("Test cafe", now);
          for (const delta of deltas)
            movement(state, {
              sku: "espresso-250",
              locationId: "roastery",
              kind: "adjusted",
              stockDelta: delta,
              reservedDelta: 0,
              cause: "test",
              at: now,
              note: "Test adjustment",
            });
          const expected = opening + deltas.reduce((a, b) => a + b, 0);
          expect(espresso(state)).toEqual({
            onHand: expected,
            reserved: 0,
            available: expected,
          });
        },
      ),
    );
  });

  it("applies a version once, even when the newest arrives first", () => {
    const state = seedDomain("Test cafe", now);
    const newest = sale("shopify:1", 4, now, "2026-10-11T10:05:00Z");
    applySale(state, newest);
    const written = state.movements.length;

    const older = sale("shopify:1", 1);
    expect(applySale(state, older).late).toBe(true);
    applySale(state, newest);

    expect(state.movements).toHaveLength(written);
    expect(espresso(state).available).toBe(opening - 4);
  });

  it("opens one incident for a late offline sale, in sale order, and resolves it by transfer", () => {
    const state = seedDomain("Test cafe", now);
    movement(state, {
      sku: "espresso-250",
      locationId: "roastery",
      kind: "adjusted",
      stockDelta: -28,
      reservedDelta: 0,
      cause: "prepare",
      at: now,
      note: "Two bags remain",
    });
    applySale(state, sale("shopify:1", 1, "2026-10-11T09:50:00Z"));
    // Made at 09:00 on the till, delivered after the 09:50 online sale.
    const offline = sale("square:1", 2, "2026-10-11T09:00:00Z");
    expect(applySale(state, offline).late).toBe(true);
    applySale(state, offline);

    expect(state.incidents).toHaveLength(1);
    const [incident] = state.incidents;
    expect(incident.sales.map((sold) => sold.source)).toEqual([
      "square",
      "shopify",
    ]);

    resolveIncident(
      state,
      incident.id,
      "transfer",
      "manager",
      "Transfer one bag",
      now,
      "cafe",
    );
    expect(espresso(state).available).toBe(0);
    expect(espresso(state, "cafe").available).toBe(19);
    expect(state.incidents[0].status).toBe("resolved");
  });

  it("keeps reservations apart from stock on hand and refuses a double release", () => {
    const state = seedDomain("Test cafe", now);
    const reservation = {
      sku: "espresso-250",
      locationId: "roastery",
      cause: "order",
      at: now,
      note: "Reserve",
    };
    movement(state, {
      ...reservation,
      kind: "reserved",
      stockDelta: 0,
      reservedDelta: 3,
    });
    expect(espresso(state)).toEqual({
      onHand: opening,
      reserved: 3,
      available: opening - 3,
    });

    const release = {
      ...reservation,
      kind: "released",
      stockDelta: 0,
      reservedDelta: -3,
    } as const;
    movement(state, release);
    expect(() => movement(state, release)).toThrow();
  });

  it("never lets refunds inflate stock, nor a later snapshot restock twice", () => {
    const state = seedDomain("Test cafe", now);
    const order = sale("shopify-refund", 2);
    applySale(state, order);
    const refund = (id: string, quantity: number) =>
      applyRefund(
        state,
        "shopify",
        id,
        order.sourceId,
        [{ sku: "espresso-250", quantity }],
        id,
        now,
      );

    refund("refund-1", 1);
    expect(espresso(state).onHand).toBe(opening - 1);
    applySale(state, { ...order, updatedAt: "2026-10-11T10:01:00Z" });
    expect(espresso(state).onHand).toBe(opening - 1);
    // One of the two bags is already back on the shelf.
    expect(() => refund("refund-2", 2)).toThrow("Restock exceeds");

    applySale(state, {
      ...order,
      cancelled: true,
      updatedAt: "2026-10-11T10:02:00Z",
    });
    expect(espresso(state).onHand).toBe(opening);
  });
});