Skip to content

Use Cases

Last Updated: 2025-01-22

Use Cases orchestrate business logic by coordinating repository calls, applying business rules, and managing complex workflows in AccessALI.


Overview

The Use Case layer sits between Server Actions and Repositories, containing business logic that's reusable across different entry points (Server Actions, API Routes, etc.).

Responsibilities

  • Orchestrate multiple repository calls
  • Apply business logic and validation
  • Handle complex workflows
  • Transform data between layers
  • Coordinate transactions

Pattern

// lib/use-cases/feature-name.ts
import { getUserProfile } from '@/lib/repositories/user-repository'
import { getUserProperties } from '@/lib/repositories/property-repository'

export async function featureNameUseCase(userId: string, params: any) {
  // 1. Validate business rules
  if (!isValidBusinessRule(params)) {
    throw new ValidationError('Invalid params')
  }

  // 2. Fetch required data (parallel when possible)
  const [user, properties] = await Promise.all([
    getUserProfile(userId),
    getUserProperties(userId),
  ])

  // 3. Apply business logic
  const result = applyBusinessLogic(user, properties, params)

  // 4. Update data if needed
  if (result.needsUpdate) {
    await updateData(result.updateData)
  }

  // 5. Return transformed result
  return transformResult(result)
}

Dashboard Use Case

Source: src/lib/use-cases/dashboard.ts

getDashboardDataUseCase

Orchestrates parallel data fetching from multiple repositories and transforms data for the dashboard view.

// lib/use-cases/dashboard.ts
import { getUserProfile } from '@/lib/repositories/user-repository'
import {
  getUserPropertiesForDashboard,
  getUserPropertyStats,
} from '@/lib/repositories/property-repository'
import { getUrgentNotifications } from '@/lib/repositories/notification-repository'
import { getRecentMessages } from '@/lib/repositories/message-repository'
import type { DashboardData } from '@/lib/types'
import { logger } from '@/lib/logger'

// Dashboard display limits
const DASHBOARD_URGENT_NOTIFICATIONS_LIMIT = 3
const DASHBOARD_RECENT_MESSAGES_LIMIT = 3

/**
 * Get complete dashboard data for authenticated user
 *
 * Orchestrates parallel fetching from 5 repositories:
 * - User profile with property count
 * - User properties (optimized query)
 * - Property statistics (single aggregate query)
 * - Urgent notifications (limit 3)
 * - Recent messages (limit 3)
 *
 * Performance: All queries run in parallel using Promise.all
 *
 * @param {string} userId - Authenticated user ID
 * @returns {Promise<DashboardData>} Complete dashboard data
 * @throws {Error} If user not found or database error
 */
export async function getDashboardDataUseCase(
  userId: string
): Promise<DashboardData> {
  try {
    // Use optimized queries - fetches all data in parallel for optimal performance
    const [userProfile, properties, propertyStats, notifications, messages] =
      await Promise.all([
        getUserProfile(userId),
        getUserPropertiesForDashboard(userId), // Optimized query with selective fields
        getUserPropertyStats(userId), // Single aggregate query for stats
        getUrgentNotifications(userId, DASHBOARD_URGENT_NOTIFICATIONS_LIMIT),
        getRecentMessages(userId, DASHBOARD_RECENT_MESSAGES_LIMIT),
      ])

    // Validate that user exists
    if (!userProfile) {
      throw new Error(`User not found: ${userId}`)
    }

    // Calculate quick action badges from notifications and messages
    const quickActionBadges = calculateQuickActionBadges(
      notifications,
      messages
    )

    // Transform data to match DashboardData interface
    return {
      user: {
        id: userProfile.id,
        firstName: userProfile.firstName ?? '',
        lastName: userProfile.lastName ?? '',
        email: userProfile.email,
        role: userProfile.role,
        profilePhoto: userProfile.profilePhoto,
        propertyCount: userProfile._count.properties,
      },
      properties: properties.map((property) => ({
        id: property.id,
        projectName: property.projectName,
        unitNumber: property.unitNumber,
        block: property.block,
        lot: property.lot,
        phase: property.phase,
        model: property.model,
        status: property.status,
        // Use deterministic random image if no image URL
        imageUrl: property.imageUrl ?? getRandomPropertyImage(property.id),
        turnoverDate: property.turnoverDate,
        currentMilestone: property.milestones[0] ?? null,
        pendingPayments: property.payments,
        documentCount: property._count.documents,
        createdAt: property.createdAt,
      })),
      propertyStats: {
        totalProperties: propertyStats.totalProperties,
        activeProperties: propertyStats.activeProperties,
        completedProperties: propertyStats.completedProperties,
        totalValue: propertyStats.totalValue,
        pendingPayments: propertyStats.pendingPayments,
        upcomingMilestones: propertyStats.upcomingMilestones,
      },
      notifications: notifications.map((notification) => ({
        id: notification.id,
        type: notification.type,
        title: notification.title,
        message: notification.message,
        priority: notification.priority,
        isRead: notification.isRead,
        createdAt: notification.createdAt,
      })),
      messages: messages.map((message) => ({
        id: message.id,
        subject: message.subject,
        preview: message.body.substring(0, 100),
        sender: message.sender,
        isUnread: !message.isRead,
        createdAt: message.createdAt,
      })),
      quickActionBadges,
    }
  } catch (error: unknown) {
    const err = error instanceof Error ? error : new Error(String(error))
    logger.error('Error in getDashboardDataUseCase', err)

    // Re-throw error to be handled by Server Action
    throw err
  }
}

/**
 * Calculate badge counts for quick action buttons
 *
 * Badges indicate:
 * - pay: Has unread payment notifications
 * - schedule: Has unread appointment notifications
 * - view: Has unread document notifications
 * - contact: Count of unread messages
 *
 * @param {Notification[]} notifications - Urgent notifications
 * @param {Message[]} messages - Recent messages
 * @returns {QuickActionBadges} Badge counts for each action
 */
function calculateQuickActionBadges(
  notifications: Notification[],
  messages: Message[]
): QuickActionBadges {
  return {
    pay: notifications.some((n) => n.type === 'PAYMENT' && !n.isRead) ? 1 : 0,
    schedule: notifications.some((n) => n.type === 'APPOINTMENT' && !n.isRead)
      ? 1
      : 0,
    view: notifications.some((n) => n.type === 'DOCUMENT' && !n.isRead) ? 1 : 0,
    contact: messages.filter((m) => !m.isRead).length,
  }
}

/**
 * Get deterministic random property image based on property ID
 *
 * Uses property ID as hash seed to ensure same image is always
 * returned for the same property across sessions.
 *
 * @param {string} propertyId - Property UUID
 * @returns {string} Property image URL
 */
export function getRandomPropertyImage(propertyId: string): string {
  // Use property ID to generate a deterministic index
  let hash = 0
  for (let i = 0; i < propertyId.length; i++) {
    const char = propertyId.charCodeAt(i)
    hash = (hash << 5) - hash + char
    hash = hash & hash // Convert to 32-bit integer
  }

  const index = Math.abs(hash) % MODERN_PROPERTY_IMAGES.length
  return MODERN_PROPERTY_IMAGES[index]
}

Performance Optimizations:

  1. Parallel Fetching: All 5 repository calls execute simultaneously
  2. Selective Fields: Repositories use Prisma select to fetch only needed fields
  3. Aggregate Query: Property stats use single aggregate query instead of multiple queries
  4. Limited Results: Notifications and messages limited to most recent/urgent

Data Flow:

getDashboardDataUseCase(userId)
    ↓ (parallel)
    ├─→ getUserProfile() → User + property count
    ├─→ getUserPropertiesForDashboard() → Properties with current milestone
    ├─→ getUserPropertyStats() → Aggregate stats (1 query)
    ├─→ getUrgentNotifications() → Top 3 urgent notifications
    └─→ getRecentMessages() → Last 3 messages
    ↓ (transform)
    DashboardData → Sent to client


Testing Use Cases

import { getDashboardDataUseCase } from '../dashboard'
import * as userRepo from '@/lib/repositories/user-repository'
import * as propertyRepo from '@/lib/repositories/property-repository'

jest.mock('@/lib/repositories/user-repository')
jest.mock('@/lib/repositories/property-repository')

describe('getDashboardDataUseCase', () => {
  it('should return complete dashboard data', async () => {
    jest.spyOn(userRepo, 'getUserProfile').mockResolvedValue(mockUser)
    jest.spyOn(propertyRepo, 'getUserProperties').mockResolvedValue(mockProperties)

    const result = await getDashboardDataUseCase('user-123')

    expect(result.user).toBeDefined()
    expect(result.properties).toHaveLength(2)
  })

  it('should throw when user not found', async () => {
    jest.spyOn(userRepo, 'getUserProfile').mockResolvedValue(null)

    await expect(getDashboardDataUseCase('invalid')).rejects.toThrow('User not found')
  })
})

Best Practices

  • Keep use cases focused on single business operations
  • Validate business rules before data access
  • Use parallel queries with Promise.all when possible
  • Re-throw repository errors unchanged
  • Transform data to match UI requirements
  • Keep use cases testable and independent