test/shared/accounting/adjustments.test.ts
Rama main · réplica de lectura del repositorio original.
import { assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import { FakeTime } from "@std/testing/time";
import { revenueAccount, WRITEOFF } from "#accounting/accounts.ts";
import { writeoffAdjustmentInserts } from "#accounting/adjustments.ts";
import { accountBalance, allTransfers } from "#accounting/queries.ts";
import { accountKey } from "#shared/ledger/account.ts";
import type { AccountRef } from "#shared/ledger/types.ts";
import {
postWriteoffAdjustment,
useTransactionalDb,
} from "#test-utils/ledger.ts";
describe("db > accounting > postWriteoffAdjustment", () => {
useTransactionalDb();
const revenue = revenueAccount(7);
/** Post one write-off adjustment to `revenue` and return its sole transfer
* leg, first asserting nothing else was posted. */
const postSoleAdjustment = async (delta: number) => {
await postWriteoffAdjustment(revenue, delta, ["income-adjust", 7]);
const [leg, ...rest] = await allTransfers();
expect(rest).toEqual([]);
return leg!;
};
test("a zero delta posts nothing", async () => {
await postWriteoffAdjustment(revenue, 0, ["income-adjust", 7]);
expect(await allTransfers()).toEqual([]);
});
/** Post one adjustment and assert the leg it wrote: how big it is, which way
* the money flowed, and the balance it left behind. Every expected value is
* spelled out, so the check never re-derives the rule it is testing. */
const expectAdjustment = async ({
amount,
balance,
delta,
from,
to,
}: {
amount: number;
balance: number;
delta: number;
from: AccountRef;
to: AccountRef;
}): Promise<void> => {
const leg = await postSoleAdjustment(delta);
expect(leg.kind).toBe("adjustment");
expect(leg.amount).toBe(amount);
expect(accountKey(leg.source)).toBe(accountKey(from));
expect(accountKey(leg.destination)).toBe(accountKey(to));
expect(await accountBalance(revenue)).toBe(balance);
};
test("a positive delta credits the account (writeoff → account)", async () => {
// Crediting the account: money flows from writeoff into the account, and
// its balance rises by the delta.
await expectAdjustment({
amount: 1500,
balance: 1500,
delta: 1500,
from: WRITEOFF,
to: revenue,
});
});
test("the smallest credit still flows into the account", async () => {
// One minor unit is a credit like any other: the direction turns on the
// sign of the delta, not on it clearing some larger figure.
await expectAdjustment({
amount: 1,
balance: 1,
delta: 1,
from: WRITEOFF,
to: revenue,
});
});
test("a negative delta debits the account (account → writeoff)", async () => {
// Debiting the account: money flows out to writeoff, its balance falls, and
// the amount posted is the magnitude of the delta.
await expectAdjustment({
amount: 1200,
balance: -1200,
delta: -1200,
from: revenue,
to: WRITEOFF,
});
});
test("amount is the absolute value of the delta either way", async () => {
await postWriteoffAdjustment(revenue, -300, ["income-adjust", 7]);
const [leg] = await allTransfers();
expect(leg!.amount).toBe(300);
});
test("repeated edits of the same figure each post a distinct event", async () => {
// Two edits of the SAME signed figure at DIFFERENT instants must each post:
// raising income by 1000, then later raising it by 1000 again. The delta is
// identical, so only the fresh `occurredAt` in the references keeps them
// distinct — without it the second would collide on `[...keyParts, delta]`
// and INSERT OR IGNORE would drop it. Freeze the clock and tick it forward a
// millisecond between posts so this exercises the occurredAt axis
// deterministically (no reliance on real wall-clock resolution). `using`
// restores the real clock at block exit even if an assertion throws, so a
// failure here never leaks the frozen clock into later tests.
using time = new FakeTime(new Date("2026-06-21T00:00:00.000Z"));
await postWriteoffAdjustment(revenue, 1000, ["income-adjust", 7]);
time.tick(1);
await postWriteoffAdjustment(revenue, 1000, ["income-adjust", 7]);
expect((await allTransfers()).length).toBe(2);
// Both raises land, so the account rises by the full 2000.
expect(await accountBalance(revenue)).toBe(2000);
});
test("two opposite corrections in the same millisecond both post", async () => {
// Freeze the clock so both posts share one millisecond `occurredAt`. Because
// the signed delta is part of the reference, the raise and the lower hash
// differently and both land — without it they would collide on
// `[...keyParts, occurredAt]` and INSERT OR IGNORE would drop the second.
// `using` restores the real clock on block exit, throw or not.
using _time = new FakeTime(new Date("2026-06-21T00:00:00.000Z"));
await postWriteoffAdjustment(revenue, 1000, ["income-adjust", 7]);
await postWriteoffAdjustment(revenue, -1000, ["income-adjust", 7]);
expect((await allTransfers()).length).toBe(2);
expect(await accountBalance(revenue)).toBe(0);
});
test("the writeoff account mirrors the opposite of the adjusted figure", async () => {
// A credit to revenue sinks from writeoff, so writeoff's own balance falls by
// the same amount — conservation holds (Σ balance == 0 across the pair).
await postWriteoffAdjustment(revenue, 800, ["income-adjust", 7]);
expect(await accountBalance(revenue)).toBe(800);
expect(await accountBalance(WRITEOFF)).toBe(-800);
});
});
describe("writeoffAdjustmentInserts (folded into a wider batch)", () => {
const revenue = revenueAccount(7);
test("builds one INSERT OR IGNORE per non-zero adjustment, dropping zero deltas", async () => {
// An attendee merge passes one adjustment per discarded booking; a zero-delta
// one (a booking that left nothing to write off) posts nothing, so only the
// non-zero adjustments become statements.
const inserts = await writeoffAdjustmentInserts(
[
{ account: revenue, delta: 1500, keyParts: ["merge", 7] },
{ account: revenue, delta: 0, keyParts: ["merge", 8] },
{ account: revenue, delta: -200, keyParts: ["merge", 9] },
],
"2026-06-21T00:00:00.000Z",
);
expect(inserts.length).toBe(2);
for (const stmt of inserts) {
assertStringIncludes(stmt.sql, "INSERT OR IGNORE");
}
});
test("an all-zero set yields no statements at all", async () => {
const inserts = await writeoffAdjustmentInserts(
[{ account: revenue, delta: 0, keyParts: ["merge", 7] }],
"2026-06-21T00:00:00.000Z",
);
expect(inserts).toEqual([]);
});
});