/** * Notification Server Actions * * Handles all notification-related server actions including: * - Fetching notifications * - Marking notifications as read * - Deleting notifications * - Checking for deadline notifications * * These actions are called from client components and provide real-time * notification management with Clerk authentication. */ "use server"; import { auth } from "@clerk/nextjs/server"; import { prisma } from "@/lib/db/prisma"; import { ContractService } from "@/lib/services/contract.service"; import { NotificationService } from "@/lib/services/notification.service"; const isNotificationTableMissingError = (error: unknown): boolean => { if (!error || typeof error !== "object") return false; const maybePrismaError = error as { code?: string; meta?: { table?: string }; message?: string; }; if (maybePrismaError.code !== "P2021") return false; const tableFromMeta = maybePrismaError.meta?.table ?? ""; const message = maybePrismaError.message ?? ""; return ( tableFromMeta.includes("Notification") || message.includes("public.Notification") ); }; /** * Fetches all unread notifications for the current authenticated user * * Uses Clerk authentication to get the current user's ID * * @param limit - Maximum number of notifications to return (default: 10) * @returns Object with success status and notifications array * * Steps: * 1. Authenticate user via Clerk * 2. Get internal user ID from database using Clerk ID * 3. Fetch unread notifications from database * 4. Include contract details (title, endDate) * 5. Return sorted by creation date (newest first) * * Example usage in component: * ```typescript * const { unreadNotifications } = await getNotifications(); * ``` */ export async function getNotifications(limit: number = 10) { try { // Step 1: Authenticate user const { userId: clerkId } = await auth(); if (!clerkId) { return { success: false, error: "Unauthorized", }; } // Step 2: Get internal user ID from database const user = await ContractService.getUserByClerkId(clerkId); if (!user) { return { success: false, error: "User not found", }; } // Step 3: Fetch unread notifications const result = await NotificationService.getUnread(user.id, limit); return result; } catch (error: unknown) { if (isNotificationTableMissingError(error)) { return { success: true, data: [], }; } console.error("Get notifications error:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Fetches complete notification history for the current user * * Returns both read and unread notifications for notification center/log view * * @param limit - Maximum number of notifications to return (default: 50) * @returns Object with success status and all notifications array */ export async function getAllNotifications(limit: number = 50) { try { const { userId: clerkId } = await auth(); if (!clerkId) { return { success: false, error: "Unauthorized", }; } const user = await ContractService.getUserByClerkId(clerkId); if (!user) { return { success: false, error: "User not found", }; } const result = await NotificationService.getAll(user.id, limit); return result; } catch (error: unknown) { if (isNotificationTableMissingError(error)) { return { success: true, data: [], }; } console.error("Get all notifications error:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Gets count of unread notifications for badge display * * Used to show badge count on notification icon in the UI * * @returns Object with unread notification count * * Example usage: * ```typescript * const { data } = await getUnreadNotificationCount(); * // Display data.count as badge on notification bell icon * ``` */ export async function getUnreadNotificationCount() { try { const { userId: clerkId } = await auth(); if (!clerkId) { return { success: false, error: "Unauthorized", }; } const user = await ContractService.getUserByClerkId(clerkId); if (!user) { return { success: false, error: "User not found", }; } const result = await NotificationService.getUnreadCount(user.id); return result; } catch (error: unknown) { if (isNotificationTableMissingError(error)) { return { success: true, data: { count: 0 }, }; } console.error("Get unread count error:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Marks a single notification as read * * @param notificationId - The ID of the notification to mark as read * @returns Object with success status * * Security: Verifies the notification belongs to the current user */ export async function markNotificationAsRead(notificationId: string) { try { const { userId: clerkId } = await auth(); if (!clerkId) { return { success: false, error: "Unauthorized", }; } const user = await ContractService.getUserByClerkId(clerkId); if (!user) { return { success: false, error: "User not found", }; } // Verify notification belongs to user const notification = await prisma.notification.findUnique({ where: { id: notificationId }, }); if (!notification || notification.userId !== user.id) { return { success: false, error: "Unauthorized", }; } const result = await NotificationService.markAsRead(notificationId); return result; } catch (error: unknown) { if (isNotificationTableMissingError(error)) { return { success: true, }; } console.error("Mark notification as read error:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Marks all unread notifications as read for the current user * * @returns Object with success status and count of updated notifications */ export async function markAllNotificationsAsRead() { try { const { userId: clerkId } = await auth(); if (!clerkId) { return { success: false, error: "Unauthorized", }; } const user = await ContractService.getUserByClerkId(clerkId); if (!user) { return { success: false, error: "User not found", }; } const result = await NotificationService.markAllAsRead(user.id); return result; } catch (error: unknown) { if (isNotificationTableMissingError(error)) { return { success: true, data: { count: 0 }, }; } console.error("Mark all notifications as read error:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Deletes a notification * * @param notificationId - The ID of the notification to delete * @returns Object with success status * * Security: Verifies the notification belongs to the current user before deletion */ export async function deleteNotification(notificationId: string) { try { const { userId: clerkId } = await auth(); if (!clerkId) { return { success: false, error: "Unauthorized", }; } const user = await ContractService.getUserByClerkId(clerkId); if (!user) { return { success: false, error: "User not found", }; } // Verify notification belongs to user const notification = await prisma.notification.findUnique({ where: { id: notificationId }, }); if (!notification || notification.userId !== user.id) { return { success: false, error: "Unauthorized", }; } const result = await NotificationService.delete(notificationId); return result; } catch (error: unknown) { if (isNotificationTableMissingError(error)) { return { success: true, }; } console.error("Delete notification error:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Checks for upcoming contract deadlines and creates notifications * * Scans all user's contracts and creates DEADLINE type notifications for: * - 30 days before expiration (CRITICAL - 🔴) * - 15 days before expiration (WARNING - 🟠) * - 7 days before expiration (URGENT - 🟡) * * @returns Object with success status and count of created notifications * * Should be called: * - On dashboard page load (to refresh deadline list) * - Once per day at a scheduled time via cron job * - When monitoring upcoming deadlines * * Example usage: * ```typescript * // In dashboard page effect * useEffect(() => { * checkDeadlineNotifications(); * }, []); * ``` */ export async function checkDeadlineNotifications() { try { const { userId: clerkId } = await auth(); if (!clerkId) { return { success: false, error: "Unauthorized", }; } const user = await ContractService.getUserByClerkId(clerkId); if (!user) { return { success: false, error: "User not found", }; } const result = await NotificationService.checkUpcomingDeadlines(user.id); return result; } catch (error: unknown) { if (isNotificationTableMissingError(error)) { return { success: true, data: { count: 0, contractIds: [] }, }; } console.error("Check deadline notifications error:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } }