feat(uat-batch-15): supplemental-info link reusable until expiry
The supplemental-info token now stays valid for re-submissions until the 14-day TTL expires. Previously the link was single-use: `applySubmission` required `consumedAt IS NULL`, which locked clients out of correcting a typo or finishing a partial submission. - Service: drops the `isNull(consumedAt)` filter; TTL is the sole validity check. `consumedAt` is still stamped on each submit so the rep / loader can see "last submitted at" context. - Public form: the "already submitted" lockout screen is removed. Instead, when the token has been used before, the form renders with the prefill (already reflecting the latest data) plus a soft amber banner noting that changes overwrite the previous submission. - Drive-by em-dash fix on the post-submit thank-you copy (matches the Wave-1 lint guard). tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,26 +157,17 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.token.consumed) {
|
||||
return (
|
||||
<BrandedAuthShell>
|
||||
<div className="text-center space-y-3 py-6">
|
||||
<CheckCircle2 className="h-10 w-10 text-emerald-600 mx-auto" />
|
||||
<h1 className="text-lg font-semibold">Thanks — we already have your details</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This form was already submitted. Your sales contact will be in touch shortly.
|
||||
</p>
|
||||
</div>
|
||||
</BrandedAuthShell>
|
||||
);
|
||||
}
|
||||
// Tokens are now reusable until expiry — the consumed flag is kept
|
||||
// so the form can show a soft "you've submitted this before" banner
|
||||
// (and prefill the entered values) without locking the recipient out
|
||||
// of updating their details.
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<BrandedAuthShell>
|
||||
<div className="text-center space-y-3 py-6">
|
||||
<CheckCircle2 className="h-10 w-10 text-emerald-600 mx-auto" />
|
||||
<h1 className="text-lg font-semibold">Thanks — got it</h1>
|
||||
<h1 className="text-lg font-semibold">Thanks, got it</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your details have been sent to the team. Watch your inbox for your EOI document shortly.
|
||||
</p>
|
||||
@@ -194,6 +185,12 @@ export default function SupplementalInfoPage({ params }: PageProps) {
|
||||
We've pre-filled what we have on file. Please review, correct anything that's
|
||||
wrong, and add what's missing.
|
||||
</p>
|
||||
{data?.token.consumed ? (
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
||||
You've submitted this form before. Feel free to update any details. Changes
|
||||
overwrite the previous submission.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<fieldset className="space-y-4">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* updates, consume token. All inside one transaction.
|
||||
*/
|
||||
|
||||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
@@ -193,14 +193,16 @@ export async function applySubmission(token: string, input: SubmissionInput): Pr
|
||||
}
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
// Reusable-until-expiry: the link stays valid for repeat
|
||||
// submissions until it expires. `consumedAt` is still stamped on
|
||||
// first submit so the rep / loader can show "last submitted at
|
||||
// <time>" context, but it no longer gates the submission. The TTL
|
||||
// gate below is the sole validity check.
|
||||
const row = await tx.query.supplementalFormTokens.findFirst({
|
||||
where: and(
|
||||
eq(supplementalFormTokens.token, token),
|
||||
isNull(supplementalFormTokens.consumedAt),
|
||||
),
|
||||
where: eq(supplementalFormTokens.token, token),
|
||||
});
|
||||
if (!row) {
|
||||
throw new ConflictError('This link has already been used or is no longer valid.');
|
||||
throw new ConflictError('This link is no longer valid.');
|
||||
}
|
||||
if (row.expiresAt.getTime() < Date.now()) {
|
||||
throw new ConflictError('This link has expired.');
|
||||
|
||||
Reference in New Issue
Block a user