diff --git a/src/lib/intake-links/submissions.test.ts b/src/lib/intake-links/submissions.test.ts new file mode 100644 index 0000000..db5f593 --- /dev/null +++ b/src/lib/intake-links/submissions.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; + +import { + computeDateForCounter, + defaultCapFor, + isDailyCapReached, + nextCount, +} from "@/lib/intake-links/submissions"; + +describe("defaultCapFor", () => { + it("returns the row cap when set, otherwise 100", () => { + expect(defaultCapFor(0)).toBe(100); + expect(defaultCapFor(50)).toBe(50); + expect(defaultCapFor(10000)).toBe(10000); + }); + + it("treats undefined as 'use default'", () => { + expect(defaultCapFor(undefined)).toBe(100); + }); +}); + +describe("isDailyCapReached", () => { + it("is false when the counter is below the cap", () => { + expect(isDailyCapReached(0, 100)).toBe(false); + expect(isDailyCapReached(99, 100)).toBe(false); + }); + it("is true when the counter equals or exceeds the cap", () => { + expect(isDailyCapReached(100, 100)).toBe(true); + expect(isDailyCapReached(101, 100)).toBe(true); + }); + it("treats cap=0 as 'no submissions today'", () => { + expect(isDailyCapReached(0, 0)).toBe(true); + }); +}); + +describe("nextCount", () => { + it("increments by one", () => { + expect(nextCount(0)).toBe(1); + expect(nextCount(7)).toBe(8); + }); +}); + +describe("computeDateForCounter", () => { + it("formats a Date as YYYY-MM-DD in local time", () => { + const d = new Date(2026, 8, 21, 12, 0, 0); + expect(computeDateForCounter(d)).toBe("2026-09-21"); + }); +}); diff --git a/src/lib/intake-links/submissions.ts b/src/lib/intake-links/submissions.ts new file mode 100644 index 0000000..46fe63c --- /dev/null +++ b/src/lib/intake-links/submissions.ts @@ -0,0 +1,40 @@ +/** + * Per-(link, project) daily-cap helpers for the Phase 9 picker path. + * + * Phase 8 stored `submission_count_today` on the link row itself, which + * silently meant "across all projects on this link". With one link per + * client, that cap stops being meaningful — one busy project could lock + * the rest of the client's projects out. Phase 9 moves the counter onto a + * new `intake_link_submissions` collection, one row per + * (link, project, date), so each project has its own daily budget. + * + * These helpers are pure. The PocketBase reads/writes live in + * `src/lib/intake-links/index.ts` next to `resolveIntakeLink`. + */ + +export const DEFAULT_DAILY_CAP = 100; + +/** Resolve the effective cap from the row's optional field. */ +export function defaultCapFor(rowCap: number | undefined): number { + if (typeof rowCap === "number" && rowCap > 0) return rowCap; + return DEFAULT_DAILY_CAP; +} + +/** True when today's count has reached or exceeded the cap. */ +export function isDailyCapReached(todayCount: number, cap: number): boolean { + if (cap <= 0) return true; // explicit zero means "no submissions today" + return todayCount >= cap; +} + +/** Increment. Pure. */ +export function nextCount(todayCount: number): number { + return todayCount + 1; +} + +/** Local-date string in YYYY-MM-DD form. Same shape as the Phase 8 helper. */ +export function computeDateForCounter(now: Date = new Date()): string { + const y = now.getFullYear(); + const m = String(now.getMonth() + 1).padStart(2, "0"); + const d = String(now.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; +}