All files / dal/rrm submissions.dal.ts

100% Statements 7/7
100% Branches 2/2
100% Functions 4/4
100% Lines 6/6

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79          18x                         13x       5x                                   3x 2x                                           8x                              
import { desc, eq, inArray } from "drizzle-orm";
import type { DrizzleD1Database } from "drizzle-orm/d1";
import * as schema from "../../db/schema";
import type { RrmSubmission } from "../../db/schema/rrm";
 
const SUBMISSIONS = schema.rrmSubmissions;
 
/**
 * `rrm_submissions` reads and the DPDP-erasure half that belongs to this
 * table (loose-ends review D3).
 *
 * No FK to `rrm_prospects` — same reasoning as `rrm_prospect_events` — but
 * unlike the event log this table is NOT append-only: `scrubForProspect`
 * exists precisely to mutate it, because a submission is where the realtor's
 * own details AND the referred contact's name/phone/project live (see the
 * column comments on `rrm_submissions` in `db/schema/rrm.ts`).
 */
export class RrmSubmissionsDal {
	constructor(private db: DrizzleD1Database<typeof schema>) {}
 
	/** One prospect's submissions, newest first. Powers the prospect detail card. */
	async findByProspectId(prospectId: string): Promise<RrmSubmission[]> {
		return await this.db
			.select()
			.from(SUBMISSIONS)
			.where(eq(SUBMISSIONS.prospectId, prospectId))
			.orderBy(desc(SUBMISSIONS.dateCreated));
	}
 
	/**
	 * Every submission for a page of prospect ids, in one query — the Sign-ups
	 * list's N+1 guard. Globally newest-first, so a caller building a
	 * per-prospect "latest submission" only needs to keep the FIRST row it
	 * sees for each `prospectId` while it walks the result once.
	 *
	 * Caller's responsibility to keep `prospectIds` well under D1's 100-bound-
	 * parameter ceiling — the Sign-ups route pages at 50 for exactly this
	 * reason (CLAUDE.md's D1 100-bind-limit note).
	 */
	async findByProspectIds(prospectIds: string[]): Promise<RrmSubmission[]> {
		if (prospectIds.length === 0) return [];
		return await this.db
			.select()
			.from(SUBMISSIONS)
			.where(inArray(SUBMISSIONS.prospectId, prospectIds))
			.orderBy(desc(SUBMISSIONS.dateCreated));
	}
 
	/**
	 * DPDP erasure, the submissions half (review D3). Nulls the realtor's own
	 * identifying fields AND the referred contact's structured details —
	 * `contactName` / `contactPhoneNorm` / `contactProject` — plus the free-text
	 * `contactNote`, which is where an unstructured phone number or name is
	 * just as likely to land. `phoneNorm` (the realtor's own, NOT NULL) and
	 * `consentVersion` (also NOT NULL, and not personal data) survive, same as
	 * `id` / `phone_norm` survive `RrmProspectsDal.erase()` — an identity
	 * anchor and the consent-basis record are not what erasure is for.
	 *
	 * Called by the `/erase` ROUTE (`routes/admin/rrm/prospects.routes.ts`),
	 * which orchestrates every store a DPDP erasure has to touch in one
	 * request — this DAL does not call itself.
	 */
	async scrubForProspect(prospectId: string): Promise<void> {
		await this.db
			.update(SUBMISSIONS)
			.set({
				name: null,
				firmName: null,
				contactName: null,
				contactPhoneNorm: null,
				contactProject: null,
				ipHash: null,
				userAgent: null,
				contactNote: null,
			})
			.where(eq(SUBMISSIONS.prospectId, prospectId));
	}
}