Ctrl K
ComunidadGuías…

schema/hrm-change-request-wipe.integration.test.ts

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

Archivo original ↗Preguntar a KiBot
/**
 * 0188 — the governed amend path clears submitted HRM change requests.
 *
 * 0185 retains every submitted request as history and refuses DELETE. That
 * rule stands for production paths; this suite proves the ONE allowance
 * 0188 adds: under `openbooks.amend = on` (fixture teardown, sandbox wipe,
 * org purge) a submitted request can be removed, so an organisation that
 * holds one is not pinned forever. Both directions are asserted against a
 * live PostgreSQL: the refusal without the GUC, the deletion with it, and
 * the real fixture teardown of an org holding a committed submitted request.
 *
 * Self-skips without OPENBOOKS_DB_URL like every DB-backed suite.
 */
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import test from "node:test";
import { sql } from "drizzle-orm";

const DB = Boolean(process.env.OPENBOOKS_DB_URL);

type Seed = { orgId: string; actorId: string; employmentId: string };

async function seed(): Promise<Seed> {
  const [{ db }, { createScratchOrg, createScratchUser }] = await Promise.all([
    import("@openbooks/engine/platform/db"),
    import("@openbooks/engine/testing/fixtures"),
  ]);
  const org = await createScratchOrg();
  assert.ok(org.customerId && org.subsidiaryId, "scratch org must seed a party and a subsidiary");
  const actorId = await createScratchUser(org.orgId, "HRM wipe actor", "hrm_wipe_actor");
  const employmentId = randomUUID();
  await db.execute(sql`
    insert into worker_employments (id, org_id, worker_party_id, employer_subsidiary_id, revision)
    values (${employmentId}, ${org.orgId}, ${org.customerId}, ${org.subsidiaryId}, 1)`);
  return { orgId: org.orgId, actorId, employmentId };
}

/** Insert a draft and submit it against a real flow run; returns the request id. */
async function submittedRequest(
  run: { execute: (q: unknown) => Promise<unknown> },
  s: Seed,
): Promise<string> {
  const id = randomUUID();
  await run.execute(sql`
    insert into hrm_employment_change_requests
      (id, org_id, employment_id, expected_employment_revision, payload,
       payload_digest, payload_schema_version, created_by)
    values (${id}, ${s.orgId}, ${s.employmentId}, 1, ${JSON.stringify({ kind: "transfer", level: 1 })}::jsonb,
      ${"0".repeat(64)}, 'v1', ${s.actorId})`);
  const flowId = randomUUID();
  await run.execute(sql`
    insert into flows (id, org_id, subject_kind, graph)
    values (${flowId}, ${s.orgId}, 'hrm_employment_change_request', '{}'::jsonb)`);
  const runId = randomUUID();
  await run.execute(sql`
    insert into flow_runs (id, org_id, flow_id, subject_kind, subject_id, trigger)
    values (${runId}, ${s.orgId}, ${flowId}, 'hrm_employment_change_request', ${id}, 'on_submit')`);
  await run.execute(sql`
    update hrm_employment_change_requests
       set status = 'pending_approval', reason = 'wipe allowance proof',
           submitted_by = ${s.actorId}, submitted_at = now(), flow_run_id = ${runId}
     where id = ${id}`);
  return id;
}

test(
  "a submitted request refuses DELETE without the amend allowance and clears with it",
  { skip: !DB, timeout: 120_000 },
  async (t) => {
    const { db } = await import("@openbooks/engine/platform/db");
    const { dropScratchOrgReporting } = await import("@openbooks/engine/testing/fixtures");
    const s = await seed();
    t.after(async () => {
      await dropScratchOrgReporting(s.orgId);
    });
    class Rollback extends Error {}
    await assert.rejects(
      db.transaction(async (tx) => {
        const id = await submittedRequest(tx, s);
        // Production shape: no GUC, the retention rule fires.
        await tx.execute(sql`savepoint no_guc`);
        await assert.rejects(
          tx.execute(sql`delete from hrm_employment_change_requests where id = ${id}`),
          (error: unknown) =>
            /retained as history/.test(
              String(error) + String((error as { cause?: unknown }).cause ?? ""),
            ),
          "submitted request must refuse DELETE without the amend allowance",
        );
        await tx.execute(sql`rollback to savepoint no_guc`);
        await tx.execute(sql`release savepoint no_guc`);
        const still = (
          await tx.execute<{ n: number }>(sql`
        select count(*)::int as n from hrm_employment_change_requests where id = ${id}`)
        ).rows[0]!.n;
        assert.equal(still, 1, "the refused DELETE removed nothing");
        // Governed amend path: the same statement clears exactly one row.
        await tx.execute(sql`select set_config('openbooks.amend', 'on', true)`);
        const deleted = (
          await tx.execute<{ id: string }>(sql`
        delete from hrm_employment_change_requests where id = ${id} returning id`)
        ).rows;
        assert.equal(deleted.length, 1, "amend allowance must delete exactly the submitted row");
        const gone = (
          await tx.execute<{ n: number }>(sql`
        select count(*)::int as n from hrm_employment_change_requests where id = ${id}`)
        ).rows[0]!.n;
        assert.equal(gone, 0);
        throw new Rollback();
      }),
      (error: unknown) => error instanceof Rollback,
    );
  },
);

test(
  "fixture teardown clears an organisation that holds a committed submitted request",
  { skip: !DB, timeout: 120_000 },
  async () => {
    const { db } = await import("@openbooks/engine/platform/db");
    const { dropScratchOrgReporting } = await import("@openbooks/engine/testing/fixtures");
    const s = await seed();
    // Committed on purpose: this is the state that pinned scratch orgs before 0188.
    const id = await submittedRequest(db, s);
    const before = (
      await db.execute<{ n: number }>(sql`
    select count(*)::int as n from hrm_employment_change_requests where org_id = ${s.orgId}`)
    ).rows[0]!.n;
    assert.equal(before, 1);
    await dropScratchOrgReporting(s.orgId);
    // Under the pooled lifecycle the org row is a reusable slot that is reset and
    // released, so the proof is that the org's HRM rows are gone — the exact
    // state that pinned scratch orgs before 0188 — not that the org row vanished.
    const after = (
      await db.execute<{ n: number }>(sql`
    select count(*)::int as n from hrm_employment_change_requests where org_id = ${s.orgId} or id = ${id}`)
    ).rows[0]!.n;
    assert.equal(after, 0, "teardown must remove the submitted request with the rest of the org");
    const runs = (
      await db.execute<{ n: number }>(sql`
    select count(*)::int as n from flow_runs where org_id = ${s.orgId}`)
    ).rows[0]!.n;
    assert.equal(runs, 0, "the bound flow run is gone with it");
    const employments = (
      await db.execute<{ n: number }>(sql`
    select count(*)::int as n from worker_employments where org_id = ${s.orgId}`)
    ).rows[0]!.n;
    assert.equal(employments, 0, "the 0184 rows are gone with it");
  },
);