124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
|
|
import { and, eq, inArray } from 'drizzle-orm';
|
||
|
|
|
||
|
|
import { db } from '@/lib/db';
|
||
|
|
import { documents, documentSigners, documentEvents } from '@/lib/db/schema/documents';
|
||
|
|
import { interests } from '@/lib/db/schema/interests';
|
||
|
|
import { ports } from '@/lib/db/schema/ports';
|
||
|
|
import { sendReminder as documensoRemind } from '@/lib/services/documenso-client';
|
||
|
|
import { logger } from '@/lib/logger';
|
||
|
|
|
||
|
|
// BR-023: Reminders only during 9-16 in port timezone, with 24h cooldown
|
||
|
|
|
||
|
|
function getCurrentHourInTimezone(timezone: string): number {
|
||
|
|
const now = new Date();
|
||
|
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
||
|
|
timeZone: timezone,
|
||
|
|
hour: 'numeric',
|
||
|
|
hour12: false,
|
||
|
|
});
|
||
|
|
return parseInt(formatter.format(now), 10);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function sendReminderIfAllowed(
|
||
|
|
documentId: string,
|
||
|
|
portId: string,
|
||
|
|
): Promise<boolean> {
|
||
|
|
const doc = await db.query.documents.findFirst({
|
||
|
|
where: and(eq(documents.id, documentId), eq(documents.portId, portId)),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!doc || !doc.interestId || !doc.documensoId) return false;
|
||
|
|
if (!['sent', 'partially_signed'].includes(doc.status)) return false;
|
||
|
|
|
||
|
|
// Check interest.reminderEnabled
|
||
|
|
const interest = await db.query.interests.findFirst({
|
||
|
|
where: eq(interests.id, doc.interestId),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!interest?.reminderEnabled) return false;
|
||
|
|
|
||
|
|
// Check port timezone
|
||
|
|
const port = await db.query.ports.findFirst({
|
||
|
|
where: eq(ports.id, portId),
|
||
|
|
});
|
||
|
|
|
||
|
|
const timezone = port?.timezone ?? 'UTC';
|
||
|
|
const currentHour = getCurrentHourInTimezone(timezone);
|
||
|
|
|
||
|
|
if (currentHour < 9 || currentHour >= 16) return false;
|
||
|
|
|
||
|
|
// Check 24h cooldown — last reminder_sent event for this document
|
||
|
|
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||
|
|
|
||
|
|
const lastReminder = await db.query.documentEvents.findFirst({
|
||
|
|
where: and(
|
||
|
|
eq(documentEvents.documentId, documentId),
|
||
|
|
eq(documentEvents.eventType, 'reminder_sent'),
|
||
|
|
),
|
||
|
|
orderBy: (de, { desc }) => [desc(de.createdAt)],
|
||
|
|
});
|
||
|
|
|
||
|
|
if (lastReminder && lastReminder.createdAt > twentyFourHoursAgo) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find current pending signer (lowest signingOrder with status='pending')
|
||
|
|
const pendingSigner = await db.query.documentSigners.findFirst({
|
||
|
|
where: and(
|
||
|
|
eq(documentSigners.documentId, documentId),
|
||
|
|
eq(documentSigners.status, 'pending'),
|
||
|
|
),
|
||
|
|
orderBy: (ds, { asc }) => [asc(ds.signingOrder)],
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!pendingSigner) return false;
|
||
|
|
|
||
|
|
// Send reminder via Documenso
|
||
|
|
try {
|
||
|
|
await documensoRemind(doc.documensoId, pendingSigner.id);
|
||
|
|
} catch (err) {
|
||
|
|
logger.error({ err, documentId, signerId: pendingSigner.id }, 'Failed to send Documenso reminder');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record event
|
||
|
|
await db.insert(documentEvents).values({
|
||
|
|
documentId,
|
||
|
|
eventType: 'reminder_sent',
|
||
|
|
signerId: pendingSigner.id,
|
||
|
|
eventData: { signerEmail: pendingSigner.signerEmail, signerRole: pendingSigner.signerRole },
|
||
|
|
});
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function processReminderQueue(portId: string): Promise<void> {
|
||
|
|
// Find all documents with status 'sent' or 'partially_signed' linked to interests with reminderEnabled=true
|
||
|
|
const activeInterests = await db.query.interests.findMany({
|
||
|
|
where: and(
|
||
|
|
eq(interests.portId, portId),
|
||
|
|
eq(interests.reminderEnabled, true),
|
||
|
|
),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (activeInterests.length === 0) return;
|
||
|
|
|
||
|
|
const interestIds = activeInterests.map((i) => i.id);
|
||
|
|
|
||
|
|
const activeDocs = await db.query.documents.findMany({
|
||
|
|
where: and(
|
||
|
|
eq(documents.portId, portId),
|
||
|
|
inArray(documents.status, ['sent', 'partially_signed']),
|
||
|
|
inArray(documents.interestId, interestIds),
|
||
|
|
),
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const doc of activeDocs) {
|
||
|
|
try {
|
||
|
|
await sendReminderIfAllowed(doc.id, portId);
|
||
|
|
} catch (err) {
|
||
|
|
logger.error({ err, documentId: doc.id, portId }, 'Reminder processing failed for document');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|