This commit is contained in:
2026-05-10 18:25:58 +01:00
parent 165af509ef
commit e4f4992a1b
31 changed files with 2708 additions and 495 deletions

View File

@@ -13,6 +13,7 @@
*/
import { prisma } from "@/lib/db/prisma";
import { EmailService } from "@/lib/services/email.service";
let hasWarnedMissingNotificationTable = false;
@@ -547,9 +548,9 @@ export class NotificationService {
* Checks for upcoming contract renewals/expirations and creates notifications
*
* Scans all contracts for a user and creates DEADLINE notifications for:
* - 30 days before expiration (CRITICAL)
* - 15 days before expiration (WARNING)
* - 7 days before expiration (URGENT)
* - 30 days before expiration
* - 14 days before expiration
* - 7 days before expiration
*
* @param userId - The user's ID
* @returns Promise with count of created notifications
@@ -557,7 +558,7 @@ export class NotificationService {
* Steps:
* 1. Query all COMPLETED contracts with endDate for the user
* 2. Calculate days until expiration
* 3. Create notification if contract expiring in 30, 15, or 7 days
* 3. Create notification if contract expiring in 30, 14, or 7 days
* 4. Check for existing notification to avoid duplicates
* 5. Return summary of created notifications
*
@@ -573,6 +574,15 @@ export class NotificationService {
const today = new Date();
today.setHours(0, 0, 0, 0);
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
email: true,
firstName: true,
lastName: true,
},
});
// Query all contracts with endDate for this user
const contracts = await prisma.contract.findMany({
where: {
@@ -610,12 +620,12 @@ export class NotificationService {
if (daysUntilExpiration === 7) {
shouldNotify = true;
level = "URGENT";
} else if (daysUntilExpiration === 15) {
} else if (daysUntilExpiration === 14) {
shouldNotify = true;
level = "WARNING";
} else if (daysUntilExpiration === 30) {
shouldNotify = true;
level = "CRITICAL";
level = "NOTICE";
}
if (shouldNotify) {
@@ -634,18 +644,21 @@ export class NotificationService {
// Only create if not already notified today
if (!existingNotification) {
const notificationTitle =
level === "CRITICAL"
? `🔴 Contract Expiring in 30 Days`
level === "NOTICE"
? "Contract renewal reminder (30 days)"
: level === "WARNING"
? `🟠 Contract Expiring in 15 Days`
: `🟡 Contract Expiring in 7 Days`;
? "Contract renewal window (14 days)"
: "Contract deadline in 7 days";
const providerLabel = contract.provider || "your provider";
const contractLabel = contract.title || "your contract";
const notificationMessage =
level === "CRITICAL"
? `${contract.title} from ${contract.provider} will expire on ${contractEnd.toLocaleDateString()}. Time to renew!`
level === "NOTICE"
? `${contractLabel} from ${providerLabel} will expire on ${contractEnd.toLocaleDateString()}. Plan your renewal.`
: level === "WARNING"
? `${contract.title} from ${contract.provider} expires in 15 days. Consider scheduling renewal.`
: `${contract.title} from ${contract.provider} expires in 7 days. Renew now!`;
? `${contractLabel} from ${providerLabel} expires in 14 days. Please review renewal steps.`
: `${contractLabel} from ${providerLabel} expires in 7 days. Action is required.`;
const result = await this.create({
userId,
@@ -654,7 +667,7 @@ export class NotificationService {
message: notificationMessage,
contractId: contract.id,
actionType: `RENEWAL_${level}`,
icon: level === "CRITICAL" ? "AlertCircle" : "AlertTriangle",
icon: level === "URGENT" ? "AlertCircle" : "AlertTriangle",
expiresIn: 24 * 60 * 60 * 1000, // 24 hours
actionData: {
level,
@@ -667,6 +680,29 @@ export class NotificationService {
if (result.success) {
createdNotifications.push(contract.id);
if (user?.email) {
try {
await EmailService.sendContractDeadlineReminderEmail({
to: user.email,
userDisplayName:
`${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() ||
null,
contractId: contract.id,
contractTitle: contract.title,
contractProvider: contract.provider,
contractEndDate: contractEnd,
daysUntilExpiration,
});
} catch (emailError) {
console.warn(
"Deadline email failed:",
emailError instanceof Error
? emailError.message
: emailError,
);
}
}
}
}
}