feat(intake-links): per-(link, project) cap read/write
Phase 9a Task 3. isLinkProjectUsable reads today's counter (or treats no row as 0). bumpDailyCapForProject upserts via the unique (link, project, date) index. The Phase 8 helpers stay live for the old shape; Phase 9b's wizard picks which one to call based on whether the link is per-project (Phase 8) or per-client (Phase 9).
This commit is contained in:
parent
c4cd1e3646
commit
b824f02f02
1 changed files with 76 additions and 1 deletions
|
|
@ -1,7 +1,16 @@
|
|||
import "server-only";
|
||||
|
||||
import { getAdminPb } from "@/lib/pb/admin";
|
||||
import type { IntakeLinkRecord } from "@/lib/pb/types";
|
||||
import type {
|
||||
IntakeLinkRecord,
|
||||
IntakeLinkSubmissionRecord,
|
||||
} from "@/lib/pb/types";
|
||||
import {
|
||||
computeDateForCounter,
|
||||
defaultCapFor,
|
||||
isDailyCapReached,
|
||||
nextCount,
|
||||
} from "@/lib/intake-links/submissions";
|
||||
|
||||
const DEFAULT_DAILY_CAP = 100;
|
||||
|
||||
|
|
@ -102,3 +111,69 @@ export async function bumpSubmissionCount(link: IntakeLinkRecord): Promise<void>
|
|||
last_submission_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 9: True when the (link, project) pair is still usable right now.
|
||||
*
|
||||
* Reads today's counter from the new `intake_link_submissions` collection.
|
||||
* On any DB read error, returns false — Phase 8 used the same conservative
|
||||
* default (a flaky read fails closed, never open).
|
||||
*/
|
||||
export async function isLinkProjectUsable(
|
||||
link: IntakeLinkRecord,
|
||||
projectId: string,
|
||||
now: Date = new Date()
|
||||
): Promise<boolean> {
|
||||
if (!link.active) return false;
|
||||
if (link.expires_at) {
|
||||
const expiry = link.expires_at.slice(0, 10);
|
||||
if (computeDateForCounter(now) > expiry) return false;
|
||||
}
|
||||
const cap = defaultCapFor(link.max_submissions_per_day);
|
||||
const today = computeDateForCounter(now);
|
||||
|
||||
const pb = await getAdminPb();
|
||||
try {
|
||||
const row = await pb
|
||||
.collection("intake_link_submissions")
|
||||
.getFirstListItem<IntakeLinkSubmissionRecord>(
|
||||
`link = "${link.id}" && project = "${projectId}" && date = "${today}"`
|
||||
);
|
||||
return !isDailyCapReached(row.count, cap);
|
||||
} catch {
|
||||
// No row for today → 0 submissions → below the cap (unless cap=0).
|
||||
return !isDailyCapReached(0, cap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 9: Increment today's counter for the (link, project) pair.
|
||||
*
|
||||
* Uses the unique `(link, project, date)` index: a fresh row is created on
|
||||
* first submission of the day, subsequent submissions update the count.
|
||||
*/
|
||||
export async function bumpDailyCapForProject(
|
||||
link: IntakeLinkRecord,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
const pb = await getAdminPb();
|
||||
const today = computeDateForCounter();
|
||||
|
||||
try {
|
||||
const existing = await pb
|
||||
.collection("intake_link_submissions")
|
||||
.getFirstListItem<IntakeLinkSubmissionRecord>(
|
||||
`link = "${link.id}" && project = "${projectId}" && date = "${today}"`
|
||||
);
|
||||
await pb.collection("intake_link_submissions").update(existing.id, {
|
||||
count: nextCount(existing.count),
|
||||
});
|
||||
} catch {
|
||||
await pb.collection("intake_link_submissions").create({
|
||||
link: link.id,
|
||||
project: projectId,
|
||||
date: today,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue