Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/shared/db/attendees/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
* Deletion for attendees.
*/

import type { InValue } from "@libsql/client";
import {
deleteByFieldStatement,
executeBatch,
queryAll,
type SqlStatement,
} from "#shared/db/client.ts";
import { ticketCountSumExpr } from "#shared/db/migrations/schema/listing-aggregates.ts";

Expand Down Expand Up @@ -40,7 +40,7 @@ const attendeeListingContributions = (

const restoreListingContributions = (
contributions: ListingContribution[],
): Array<{ sql: string; args: InValue[] }> =>
): SqlStatement[] =>
contributions.map((row) => ({
args: [row.booked_quantity, row.tickets_count, row.listing_id],
sql: `UPDATE listings
Expand All @@ -59,15 +59,22 @@ const DEPENDENT_ROW_TARGETS = [
{ field: "servicing_attendee_id", table: "service_costs" },
] as const;

/** Build the common dependent-row deletes for one or many attendee ids. */
export const attendeeDependentDeleteStatements = (
attendeeIds: SqlStatement,
): SqlStatement[] =>
DEPENDENT_ROW_TARGETS.map(({ field, table }) => ({
args: attendeeIds.args,
sql: `DELETE FROM ${table} WHERE ${field} IN (${attendeeIds.sql})`,
}));

/** Delete an attendee and all dependent data tied to the attendee record. */
const purgeAttendee = (
attendeeId: number,
contributions: ListingContribution[],
): Promise<void> =>
executeBatch([
...DEPENDENT_ROW_TARGETS.map((target) =>
deleteByFieldStatement({ ...target, value: attendeeId }),
),
...attendeeDependentDeleteStatements({ args: [attendeeId], sql: "?" }),
...restoreListingContributions(contributions),
deleteByFieldStatement({
field: "id",
Expand Down
23 changes: 4 additions & 19 deletions src/shared/db/orphan-attendees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* listing" when the underlying row is gone.
*/

import { attendeeDependentDeleteStatements } from "#shared/db/attendees/delete.ts";
import { executeBatchWithResults, queryOne } from "#shared/db/client.ts";

/**
Expand All @@ -35,16 +36,6 @@ const ORPHAN_IDS = `SELECT attendee.id
WHERE booking.attendee_id = attendee.id
)`;

/** Dependent tables keyed by attendee_id, cleared before the attendees rows.
* Mirrors the canonical deleteAttendee purge set (listing_attendees is empty
* for a true orphan, but is included for exact parity and race safety). */
const ORPHAN_DEPENDENT_TABLES = [
"processed_payments",
"attendee_answers",
"listing_attendees",
"system_notes",
] as const;

/** Count orphaned attendees whose `created` is before `cutoffIso`. */
export const countOrphanedAttendees = async (
cutoffIso: string,
Expand All @@ -67,16 +58,10 @@ export const purgeOrphanedAttendees = async (
cutoffIso: string,
): Promise<number> => {
const statements = [
...ORPHAN_DEPENDENT_TABLES.map((table) => ({
args: [cutoffIso],
sql: `DELETE FROM ${table} WHERE attendee_id IN (${ORPHAN_IDS})`,
})),
// service_costs uses servicing_attendee_id (not attendee_id), so it cannot
// be in ORPHAN_DEPENDENT_TABLES; handle it separately to match deleteAttendee.
{
...attendeeDependentDeleteStatements({
args: [cutoffIso],
sql: `DELETE FROM service_costs WHERE servicing_attendee_id IN (${ORPHAN_IDS})`,
},
sql: ORPHAN_IDS,
}),
{
args: [cutoffIso],
sql: `DELETE FROM attendees WHERE id IN (${ORPHAN_IDS})`,
Expand Down
6 changes: 3 additions & 3 deletions test/integration/servicing/purge-edge-cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
* Behaviour under test (all shipped):
* - `purgeOrphanedAttendees` deletes `system_notes` rows for swept orphans
* (`system_notes` is in `ORPHAN_DEPENDENT_TABLES`).
* (`system_notes` is a dependent row cleared by the shared attendee purge set).
* - A cost-bearing servicing event purged as an orphan leaves its cost legs
* as orphaned history — the transfers ledger is append-only, so the purge
* never reverses them.
Expand All @@ -33,7 +33,7 @@ import {

// jscpd:ignore-end

/** Insert a system_notes row for an attendee (the table the purge omits). */
/** Insert a system_notes row for an attendee (a dependent row the purge clears). */
const attachSystemNote = async (attendeeId: number): Promise<void> => {
await getDb().execute({
args: [attendeeId],
Expand Down Expand Up @@ -85,7 +85,7 @@ describeWithEnv("servicing edge cases — purge", { db: true }, () => {
await attachSystemNote(id);
expect(await childRowCount("system_notes", id)).toBe(1);
await purgeOrphanedAttendees(nowIso());
// system_notes is in ORPHAN_DEPENDENT_TABLES, so the purge clears it.
// system_notes is a dependent row, so the shared purge clears it.
expect(await childRowCount("system_notes", id)).toBe(0);
});

Expand Down
5 changes: 4 additions & 1 deletion test/shared/db/orphan-attendees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
countOrphanedAttendees,
purgeOrphanedAttendees,
} from "#shared/db/orphan-attendees.ts";
import { createSystemNote, getNoteRows } from "#shared/db/system-notes.ts";
import { nowIso, nowMs } from "#shared/now.ts";
import { describeWithEnv } from "#test-utils/db.ts";
import { createTestAttendeeDirect } from "#test-utils/db-helpers/attendees.ts";
Expand Down Expand Up @@ -125,7 +126,7 @@ describeWithEnv("db > orphan-attendees", { db: true }, () => {
expect(remaining?.c).toBe(0);
});

test("removes the orphan's dependent answer and payment rows", async () => {
test("removes the orphan's dependent rows", async () => {
const id = await insertOrphan(daysAgoIso(365));
await getDb().execute(
insert("attendee_answers", {
Expand All @@ -141,11 +142,13 @@ describeWithEnv("db > orphan-attendees", { db: true }, () => {
processed_at: nowIso(),
}),
);
await createSystemNote(id, "orphan note");

await purgeOrphanedAttendees(nowIso());

expect(await childCount("attendee_answers", id)).toBe(0);
expect(await childCount("processed_payments", id)).toBe(0);
expect(await getNoteRows([id])).toEqual([]);
});
});
});