feat(reminders): Phase 4 partial — schema + service + validators
Migration 0072 — reminders/interests expansion: - interests.reminder_note: optional cadence note for the existing reminderEnabled+reminderDays flow. Surfaces in notification body + inbox row. - reminders.yacht_id (+ FK + relation): fourth entity link so yacht-scoped tasks have a typed home alongside client/interest/berth. - reminders.fired_at: worker idempotency. Partial index idx_reminders_due_unfired drives the scan. Service + validator updates: - createReminderSchema / updateReminderSchema accept yachtId. - assertReminderFksInPort validates yacht ownership against the caller's port — defense-in-depth, same shape as other entity FKs. - createReminder / updateReminder thread yachtId through. Worker scheduler + CreateReminderDialog yachtId UI deferred. The existing reminders/reminder-form.tsx already covers the dialog contract — Phase 4b extends it with yachtId + the per-user digest_time_of_day picker. Tests: 1374/1374 passing. tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { and, eq, lte, gte, desc, asc, inArray, sql } from 'drizzle-orm';
|
||||
import { db } from '@/lib/db';
|
||||
import { reminders, interests, clients } from '@/lib/db/schema';
|
||||
import { berths } from '@/lib/db/schema/berths';
|
||||
import { yachts } from '@/lib/db/schema/yachts';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { NotFoundError, ValidationError } from '@/lib/errors';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
@@ -118,7 +119,12 @@ export async function getUpcomingReminders(portId: string, days: number = 14) {
|
||||
*/
|
||||
async function assertReminderFksInPort(
|
||||
portId: string,
|
||||
fks: { clientId?: string | null; interestId?: string | null; berthId?: string | null },
|
||||
fks: {
|
||||
clientId?: string | null;
|
||||
interestId?: string | null;
|
||||
berthId?: string | null;
|
||||
yachtId?: string | null;
|
||||
},
|
||||
): Promise<void> {
|
||||
const checks: Array<Promise<void>> = [];
|
||||
if (fks.clientId) {
|
||||
@@ -150,6 +156,15 @@ async function assertReminderFksInPort(
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (fks.yachtId) {
|
||||
checks.push(
|
||||
db.query.yachts
|
||||
.findFirst({ where: and(eq(yachts.id, fks.yachtId), eq(yachts.portId, portId)) })
|
||||
.then((row) => {
|
||||
if (!row) throw new ValidationError('yachtId not found in this port');
|
||||
}),
|
||||
);
|
||||
}
|
||||
await Promise.all(checks);
|
||||
}
|
||||
|
||||
@@ -169,6 +184,7 @@ export async function createReminder(portId: string, data: CreateReminderInput,
|
||||
clientId: data.clientId,
|
||||
interestId: data.interestId,
|
||||
berthId: data.berthId,
|
||||
yachtId: data.yachtId,
|
||||
});
|
||||
|
||||
const [reminder] = await db
|
||||
@@ -184,6 +200,7 @@ export async function createReminder(portId: string, data: CreateReminderInput,
|
||||
clientId: data.clientId ?? null,
|
||||
interestId: data.interestId ?? null,
|
||||
berthId: data.berthId ?? null,
|
||||
yachtId: data.yachtId ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -237,12 +254,14 @@ export async function updateReminder(
|
||||
if (data.clientId !== undefined) updates.clientId = data.clientId;
|
||||
if (data.interestId !== undefined) updates.interestId = data.interestId;
|
||||
if (data.berthId !== undefined) updates.berthId = data.berthId;
|
||||
if (data.yachtId !== undefined) updates.yachtId = data.yachtId;
|
||||
|
||||
// Re-validate any subject-FK changes against the caller's port.
|
||||
await assertReminderFksInPort(portId, {
|
||||
clientId: data.clientId,
|
||||
interestId: data.interestId,
|
||||
berthId: data.berthId,
|
||||
yachtId: data.yachtId,
|
||||
});
|
||||
|
||||
const [updated] = await db
|
||||
|
||||
Reference in New Issue
Block a user