Ctrl K
ComunidadGuías…

test/shared/accounting/reverses-group.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { assertStringIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { it as test } from "@std/testing/bdd";
import { attendeeAccount, revenueAccount } from "#accounting/accounts.ts";
import { KIND } from "#accounting/kinds.ts";
import { backfillReversesGroup } from "#accounting/reverses-group.ts";
import { postTransfers } from "#accounting/store.ts";
import { getDb } from "#db/client.ts";
import { enableQueryLog, getQueryLog } from "#db/query-log.ts";
import type { TransferInput } from "#shared/ledger/types.ts";
import {
  expectStampedToBooking,
  forceLegLink,
  refundGroupOfBooking,
  refundLegLinks,
  seedUnattributedRefund,
  stampedReversesOf,
} from "#test/shared/accounting/reverses-group/helpers.ts";
import { describeWithEnv } from "#test-utils/db.ts";
import { withDbFault } from "#test-utils/db-fault.ts";
import { withRequestContext } from "#test-utils/request-context.ts";
import { tx } from "#test-utils/transfer-factory.ts";

/** A refund leg no derivation can attribute — an event group nothing derived
 *  ever lands on. `suffix` keeps several orphans' groups distinct. */
const orphanRefundLeg = (suffix = ""): TransferInput =>
  tx({
    amount: 100,
    destination: attendeeAccount(9),
    eventGroup: `evt-orphan-refund${suffix}`,
    kind: KIND.refundSale,
    reference: `orphan-refund-sale${suffix}`,
    source: revenueAccount(99),
  });

/** The stored cursor checkpoint, or "" when no run is paused. */
const storedCursor = async (): Promise<string> => {
  const rows = await getDb().execute({
    args: ["backfill_reverses_group_cursor"],
    sql: "SELECT value FROM settings WHERE key = ?",
  });
  return String(rows.rows[0]?.value ?? "");
};

describeWithEnv("accounting > reverses-group backfill", { db: true }, () => {
  test("attributes every stored refund leg to the order it reversed", async () => {
    const bookingGroup = await seedUnattributedRefund(
      "reverses-group-order",
      7,
    );

    await backfillReversesGroup();

    expect(
      await stampedReversesOf(await refundGroupOfBooking(bookingGroup)),
    ).toEqual(new Set([bookingGroup]));
  });

  test("stamps several orders' reversals in the same run", async () => {
    const first = await seedUnattributedRefund("reverses-group-order", 7);
    const second = await seedUnattributedRefund("reverses-group-second", 8);

    await backfillReversesGroup();

    await expectStampedToBooking(first);
    await expectStampedToBooking(second);
  });

  test("is idempotent: a re-run writes nothing and still verifies", async () => {
    const bookingGroup = await seedUnattributedRefund(
      "reverses-group-rerun",
      8,
    );
    await backfillReversesGroup();

    await backfillReversesGroup();

    expect(
      await stampedReversesOf(await refundGroupOfBooking(bookingGroup)),
    ).toEqual(new Set([bookingGroup]));
  });

  test("resumes at its checkpoint after a mid-walk stop", async () => {
    // A page-size of 1 puts each order on its own page. A trigger that
    // aborts the SECOND page's stamp models a crash mid-walk: the first
    // page is committed, its checkpoint written, the rest unprocessed.
    const first = await seedUnattributedRefund("reverses-group-page-a", 7);
    const second = await seedUnattributedRefund("reverses-group-page-b", 8);
    const laterGroup = [first, second].sort()[1]!;
    const stampsOf = async (bookingGroup: string): Promise<Set<string>> =>
      stampedReversesOf(await refundGroupOfBooking(bookingGroup));

    await expect(
      withDbFault(
        `CREATE TRIGGER test_reverses_page_stop
          BEFORE UPDATE ON transfers
          WHEN NEW.reverses_group = '${laterGroup}'
          BEGIN SELECT RAISE(ABORT, 'stop before the later page'); END`,
        "test_reverses_page_stop",
        () => backfillReversesGroup(1),
      ),
    ).rejects.toThrow("stop before the later page");

    // One page committed and the checkpoint names it; the run stopped with
    // the other order's link still empty.
    expect(
      (await stampsOf(first)).has(first) !==
        (await stampsOf(second)).has(second),
    ).toBe(true);
    expect(await storedCursor()).not.toBe("");

    await backfillReversesGroup(1);

    await expectStampedToBooking(first);
    await expectStampedToBooking(second);
    // A finished run leaves the terminal mark, not a walk position.
    expect(await storedCursor()).toBe("complete");
  });

  test("a clean finish keeps a terminal mark a retry reads instead of re-walking", async () => {
    // The migration runner verifies and marks the migration only AFTER up()
    // returns. A request whose database budget ended exactly at the finish
    // never verified, so its retry reads this mark and skips the walk — a
    // deleted mark would make that retry re-walk every page, and the same
    // budget alignment would repeat forever and block the upgrade.
    const bookingGroup = await seedUnattributedRefund(
      "reverses-group-terminal",
      7,
    );
    await backfillReversesGroup();

    expect(await storedCursor()).toBe("complete");

    await withRequestContext(async () => {
      enableQueryLog();
      await backfillReversesGroup();
      const ran = getQueryLog().map((entry) => entry.sql);
      expect(ran.length).toBe(1);
      assertStringIncludes(ran[0] ?? "", "FROM settings WHERE key = ?");
    });
    await expectStampedToBooking(bookingGroup);
  });

  test("fills only empty links, never one an operator already set", async () => {
    const bookingGroup = await seedUnattributedRefund(
      "reverses-group-preset",
      7,
    );
    const refundGroup = await refundGroupOfBooking(bookingGroup);
    const legs = await refundLegLinks(refundGroup);
    // An operator's repair holds a foreign link: the backfill must leave it
    // alone even while attributing the sibling legs it came in with.
    const repaired = legs[0]!;
    await forceLegLink(repaired.id, "evt-operator-repair");

    await backfillReversesGroup();

    const after = await refundLegLinks(refundGroup);
    expect(after.find((leg) => leg.id === repaired.id)!.reverses_group).toBe(
      "evt-operator-repair",
    );
    for (const leg of after) {
      if (leg.id === repaired.id) continue;
      expect(leg.reverses_group).toBe(bookingGroup);
    }
  });

  test("names the orphan refund event and fails rather than guess", async () => {
    await postTransfers([orphanRefundLeg()]);

    await expect(backfillReversesGroup()).rejects.toThrow(
      "refund legs with no booking order they reverse: evt-orphan-refund" +
        " — repair the orphaned refund legs or their missing order, then re-run",
    );
  });

  test("names every orphan in one comma-separated report", async () => {
    // Each orphan event group is its own post: the store refuses one call
    // carrying legs from two events.
    await postTransfers([orphanRefundLeg()]);
    await postTransfers([orphanRefundLeg("-two")]);

    await expect(backfillReversesGroup()).rejects.toThrow(
      "refund legs with no booking order they reverse: evt-orphan-refund," +
        " evt-orphan-refund-two — repair the orphaned refund legs or their" +
        " missing order, then re-run",
    );
  });

  test("refuses a page size that would walk nothing and still mark complete", async () => {
    // A page size of 0 walks no page and scans no orphan, so without the
    // refusal it would write the terminal mark over unstamped legs and the
    // walk would stay skipped forever.
    const bookingGroup = await seedUnattributedRefund(
      "reverses-group-zero-page",
      7,
    );

    await expect(backfillReversesGroup(0)).rejects.toThrow(
      "backfillReversesGroup: pageSize must be a positive whole number, got 0",
    );

    expect(await storedCursor()).toBe("");
    expect(
      await stampedReversesOf(await refundGroupOfBooking(bookingGroup)),
    ).toEqual(new Set([""]));
  });

  test("an orphan refusal leaves no checkpoint behind", async () => {
    // Page size 1 makes the walk checkpoint real pages before the final scan
    // refuses on the orphan.
    const first = await seedUnattributedRefund("reverses-group-refuse-a", 7);
    const second = await seedUnattributedRefund("reverses-group-refuse-b", 8);
    await postTransfers([orphanRefundLeg()]);

    await expect(backfillReversesGroup(1)).rejects.toThrow(
      "refund legs with no booking order they reverse: evt-orphan-refund",
    );

    // The refusal is a finished run: an operator repairs an orphan by adding
    // legs that sort before the walked cursor, so a left-behind checkpoint
    // would make the retried walk skip the repaired region forever.
    expect(await storedCursor()).toBe("");
    await expectStampedToBooking(first);
    await expectStampedToBooking(second);
  });
});